From a6b2751936797b5b27a4f9a5a5919e8c43a1ba1f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 23:25:27 +0100 Subject: [PATCH 01/13] Cleanup CosmosSdkTx --- packages/bcp/src/decode.ts | 4 ++-- packages/bcp/src/encode.ts | 4 ++-- packages/bcp/types/encode.d.ts | 4 ++-- packages/sdk/src/decoding.ts | 4 ++-- packages/sdk/src/restclient.ts | 8 ++++---- packages/sdk/src/types.ts | 18 +++++++----------- packages/sdk/types/restclient.d.ts | 4 ++-- packages/sdk/types/types.d.ts | 12 +++++------- 8 files changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/bcp/src/decode.ts b/packages/bcp/src/decode.ts index 03b1b7fd..bd4a115d 100644 --- a/packages/bcp/src/decode.ts +++ b/packages/bcp/src/decode.ts @@ -109,8 +109,8 @@ export function parseTx( nonce: Nonce, tokens: BankTokens, ): SignedTransaction { - if (!types.isAminoStdTx(txValue)) { - throw new Error("Only Amino StdTx is supported"); + if (!types.isStdTx(txValue)) { + throw new Error("Only StdTx is supported"); } if (txValue.msg.length !== 1) { throw new Error("Only single-message transactions currently supported"); diff --git a/packages/bcp/src/encode.ts b/packages/bcp/src/encode.ts index 1cf8a300..ca606156 100644 --- a/packages/bcp/src/encode.ts +++ b/packages/bcp/src/encode.ts @@ -81,7 +81,7 @@ export function buildUnsignedTx( tx: UnsignedTransaction, bankTokens: BankTokens, erc20Tokens: readonly Erc20Token[] = [], -): types.AminoTx { +): types.CosmosSdkTx { if (!isSendTransaction(tx)) { throw new Error("Received transaction of unsupported kind"); } @@ -152,7 +152,7 @@ export function buildSignedTx( tx: SignedTransaction, bankTokens: BankTokens, erc20Tokens: readonly Erc20Token[] = [], -): types.AminoTx { +): types.CosmosSdkTx { const built = buildUnsignedTx(tx.transaction, bankTokens, erc20Tokens); return { ...built, diff --git a/packages/bcp/types/encode.d.ts b/packages/bcp/types/encode.d.ts index bf6d0410..3255e55f 100644 --- a/packages/bcp/types/encode.d.ts +++ b/packages/bcp/types/encode.d.ts @@ -10,9 +10,9 @@ export declare function buildUnsignedTx( tx: UnsignedTransaction, bankTokens: BankTokens, erc20Tokens?: readonly Erc20Token[], -): types.AminoTx; +): types.CosmosSdkTx; export declare function buildSignedTx( tx: SignedTransaction, bankTokens: BankTokens, erc20Tokens?: readonly Erc20Token[], -): types.AminoTx; +): types.CosmosSdkTx; diff --git a/packages/sdk/src/decoding.ts b/packages/sdk/src/decoding.ts index 11560d12..78286018 100644 --- a/packages/sdk/src/decoding.ts +++ b/packages/sdk/src/decoding.ts @@ -1,10 +1,10 @@ import { Encoding } from "@iov/encoding"; -import { isAminoStdTx, StdTx } from "./types"; +import { isStdTx, StdTx } from "./types"; export function unmarshalTx(data: Uint8Array): StdTx { const decoded = JSON.parse(Encoding.fromUtf8(data)); - if (!isAminoStdTx(decoded)) { + if (!isStdTx(decoded)) { throw new Error("Must be json encoded StdTx"); } return decoded; diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 14857bdb..b6ee7e05 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -2,11 +2,11 @@ import { Encoding } from "@iov/encoding"; import axios, { AxiosError, AxiosInstance } from "axios"; import { - AminoTx, CodeInfo, ContractInfo, CosmosSdkAccount, - isAminoStdTx, + CosmosSdkTx, + isStdTx, Model, parseWasmData, StdTx, @@ -70,7 +70,7 @@ export interface TxsResponse { readonly height: string; readonly txhash: string; readonly raw_log: string; - readonly tx: AminoTx; + readonly tx: CosmosSdkTx; } interface SearchTxsResponse { @@ -260,7 +260,7 @@ export class RestClient { public async postTx(tx: Uint8Array): Promise { // TODO: check this is StdTx const decoded = JSON.parse(fromUtf8(tx)); - if (!isAminoStdTx(decoded)) { + if (!isStdTx(decoded)) { throw new Error("Must be json encoded StdTx"); } const params = { diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c7fcfe0f..9ec7c3d3 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -2,14 +2,7 @@ import { Encoding } from "@iov/encoding"; const { fromBase64, fromHex } = Encoding; -// We will move all needed *interfaces* from amino-js here -// This means bcp can just import them from here (if needed at all) -export interface Tx { - readonly type: string; - // TODO - readonly value: unknown; -} - +/** An Amino/Cosmos SDK StdTx */ export interface StdTx { readonly msg: ReadonlyArray; readonly fee: StdFee; @@ -17,15 +10,18 @@ export interface StdTx { readonly memo: string | undefined; } -export type AminoTx = Tx & { readonly value: StdTx }; - -export function isAminoStdTx(txValue: unknown): txValue is StdTx { +export function isStdTx(txValue: unknown): txValue is StdTx { const { memo, msg, fee, signatures } = txValue as StdTx; return ( typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures) ); } +export interface CosmosSdkTx { + readonly type: string; + readonly value: StdTx; +} + interface MsgTemplate { readonly type: string; readonly value: object; diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index b83f731a..8f41d03a 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -1,4 +1,4 @@ -import { AminoTx, CodeInfo, ContractInfo, CosmosSdkAccount, Model, StdTx } from "./types"; +import { CodeInfo, ContractInfo, CosmosSdkAccount, CosmosSdkTx, Model, StdTx } from "./types"; interface NodeInfo { readonly network: string; } @@ -42,7 +42,7 @@ export interface TxsResponse { readonly height: string; readonly txhash: string; readonly raw_log: string; - readonly tx: AminoTx; + readonly tx: CosmosSdkTx; } interface SearchTxsResponse { readonly total_count: string; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index 9d61f398..af31396b 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -1,17 +1,15 @@ -export interface Tx { - readonly type: string; - readonly value: unknown; -} +/** An Amino/Cosmos SDK StdTx */ export interface StdTx { readonly msg: ReadonlyArray; readonly fee: StdFee; readonly signatures: ReadonlyArray; readonly memo: string | undefined; } -export declare type AminoTx = Tx & { +export declare function isStdTx(txValue: unknown): txValue is StdTx; +export interface CosmosSdkTx { + readonly type: string; readonly value: StdTx; -}; -export declare function isAminoStdTx(txValue: unknown): txValue is StdTx; +} interface MsgTemplate { readonly type: string; readonly value: object; From 80d0c4461660c0bb27894c4e52ea1cda3fce425e Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 00:23:10 +0100 Subject: [PATCH 02/13] Move test code to testutils.spec --- packages/sdk/src/leb128.spec.ts | 39 ---------------- packages/sdk/src/restclient.spec.ts | 41 ++-------------- packages/sdk/src/testutils.spec.ts | 72 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 75 deletions(-) delete mode 100644 packages/sdk/src/leb128.spec.ts create mode 100644 packages/sdk/src/testutils.spec.ts diff --git a/packages/sdk/src/leb128.spec.ts b/packages/sdk/src/leb128.spec.ts deleted file mode 100644 index c95f8e38..00000000 --- a/packages/sdk/src/leb128.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Encoding } from "@iov/encoding"; - -const { fromHex } = Encoding; - -export function leb128Encode(uint: number): Uint8Array { - if (uint < 0) throw new Error("Only non-negative values supported"); - if (uint > 0x7fffffff) throw new Error("Only values in signed int32 range allowed"); - const out = new Array(); - let value = uint; - do { - // tslint:disable: no-bitwise - let byte = value & 0b01111111; - value >>= 7; - - // more bytes to come: set high order bit of byte - if (value !== 0) byte ^= 0b10000000; - - out.push(byte); - // tslint:enable: no-bitwise - } while (value !== 0); - return new Uint8Array(out); -} - -describe("leb128", () => { - describe("leb128Encode", () => { - it("works for single byte values", () => { - // Values in 7 bit range are encoded as one byte - expect(leb128Encode(0)).toEqual(fromHex("00")); - expect(leb128Encode(20)).toEqual(fromHex("14")); - expect(leb128Encode(127)).toEqual(fromHex("7f")); - }); - - it("works for multi byte values", () => { - // from external souce (wasm-objdump) - expect(leb128Encode(145)).toEqual(fromHex("9101")); - expect(leb128Encode(1539)).toEqual(fromHex("830c")); - }); - }); -}); diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 3cb01de3..c0930d07 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -1,16 +1,15 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { Random, Sha256 } from "@iov/crypto"; -import { Bech32, Encoding } from "@iov/encoding"; +import { Sha256 } from "@iov/crypto"; +import { Encoding } from "@iov/encoding"; import { assert } from "@iov/utils"; import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; -import { leb128Encode } from "./leb128.spec"; import { findAttribute, parseLogs } from "./logs"; import { Pen, Secp256k1Pen } from "./pen"; import { encodeBech32Pubkey } from "./pubkey"; import { PostTxsResponse, RestClient } from "./restclient"; -import contract from "./testdata/contract.json"; import cosmoshub from "./testdata/cosmoshub.json"; +import { getRandomizedHackatom, makeRandomAddress } from "./testutils.spec"; import { Coin, Msg, @@ -60,36 +59,6 @@ function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: }; } -function getRandomizedContract(): Uint8Array { - const data = fromBase64(contract.data); - // 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] : - // 00e67d: 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) - - // Any unsigned integer from 128 to 16383 is encoded to two leb128 bytes - const min = 128; - const max = 16383; - const random = Math.floor(Math.random() * (max - min)) + min; - const bytes = leb128Encode(random); - - data[0x00e67d + 1] = bytes[0]; - data[0x00e67d + 2] = bytes[1]; - - return data; -} - -function makeRandomAddress(): string { - return Bech32.encode("cosmos", Random.getBytes(20)); -} - async function uploadCustomContract( client: RestClient, pen: Pen, @@ -123,7 +92,7 @@ async function uploadCustomContract( } async function uploadContract(client: RestClient, pen: Pen): Promise { - return uploadCustomContract(client, pen, getRandomizedContract()); + return uploadCustomContract(client, pen, getRandomizedHackatom()); } async function instantiateContract( @@ -376,7 +345,7 @@ describe("RestClient", () => { const numExisting = existingInfos.length; // upload data - const wasmCode = getRandomizedContract(); + const wasmCode = getRandomizedHackatom(); const result = await uploadCustomContract(client, pen, wasmCode); expect(result.code).toBeFalsy(); const logs = parseLogs(result.logs); diff --git a/packages/sdk/src/testutils.spec.ts b/packages/sdk/src/testutils.spec.ts new file mode 100644 index 00000000..5e099de6 --- /dev/null +++ b/packages/sdk/src/testutils.spec.ts @@ -0,0 +1,72 @@ +import { Random } from "@iov/crypto"; +import { Bech32, Encoding } from "@iov/encoding"; + +import hackatom from "./testdata/contract.json"; + +const { fromHex } = Encoding; + +export function leb128Encode(uint: number): Uint8Array { + if (uint < 0) throw new Error("Only non-negative values supported"); + if (uint > 0x7fffffff) throw new Error("Only values in signed int32 range allowed"); + const out = new Array(); + let value = uint; + do { + // tslint:disable: no-bitwise + let byte = value & 0b01111111; + value >>= 7; + + // more bytes to come: set high order bit of byte + if (value !== 0) byte ^= 0b10000000; + + out.push(byte); + // tslint:enable: no-bitwise + } while (value !== 0); + return new Uint8Array(out); +} + +export function getRandomizedHackatom(): Uint8Array { + const data = Encoding.fromBase64(hackatom.data); + // 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] : + // 00e67d: 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) + + // Any unsigned integer from 128 to 16383 is encoded to two leb128 bytes + const min = 128; + const max = 16383; + const random = Math.floor(Math.random() * (max - min)) + min; + const bytes = leb128Encode(random); + + data[0x00e67d + 1] = bytes[0]; + data[0x00e67d + 2] = bytes[1]; + + return data; +} + +export function makeRandomAddress(): string { + return Bech32.encode("cosmos", Random.getBytes(20)); +} + +describe("leb128", () => { + describe("leb128Encode", () => { + it("works for single byte values", () => { + // Values in 7 bit range are encoded as one byte + expect(leb128Encode(0)).toEqual(fromHex("00")); + expect(leb128Encode(20)).toEqual(fromHex("14")); + expect(leb128Encode(127)).toEqual(fromHex("7f")); + }); + + it("works for multi byte values", () => { + // from external souce (wasm-objdump) + expect(leb128Encode(145)).toEqual(fromHex("9101")); + expect(leb128Encode(1539)).toEqual(fromHex("830c")); + }); + }); +}); From 66fd0800ff6e26b008dad54d1168c964ab2fd3b6 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 00:49:59 +0100 Subject: [PATCH 03/13] Add first version of CosmWasmClient --- packages/sdk/src/cosmwasmclient.spec.ts | 112 ++++++++++++++++++ packages/sdk/src/cosmwasmclient.ts | 147 ++++++++++++++++++++++++ packages/sdk/src/index.ts | 1 + packages/sdk/types/cosmwasmclient.d.ts | 22 ++++ packages/sdk/types/index.d.ts | 1 + 5 files changed, 283 insertions(+) create mode 100644 packages/sdk/src/cosmwasmclient.spec.ts create mode 100644 packages/sdk/src/cosmwasmclient.ts create mode 100644 packages/sdk/types/cosmwasmclient.d.ts diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts new file mode 100644 index 00000000..a513d2f3 --- /dev/null +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -0,0 +1,112 @@ +import { CosmWasmClient } from "./cosmwasmclient"; +import { encodeSecp256k1Signature } from "./encoding"; +import { Secp256k1Pen } from "./pen"; +import { RestClient } from "./restclient"; +import { getRandomizedHackatom, makeRandomAddress } from "./testutils.spec"; +import { Coin } from "./types"; + +const httpUrl = "http://localhost:1317"; + +function cosmosEnabled(): boolean { + return !!process.env.COSMOS_ENABLED; +} + +function pendingWithoutCosmos(): void { + if (!cosmosEnabled()) { + return pending("Set COSMOS_ENABLED to enable Cosmos node-based tests"); + } +} + +const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }, + address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", +}; + +describe("CosmWasmClient", () => { + describe("makeReadOnly", () => { + it("can be constructed", () => { + const client = CosmWasmClient.makeReadOnly(httpUrl); + expect(client).toBeTruthy(); + }); + }); + + describe("chainId", () => { + it("works", async () => { + pendingWithoutCosmos(); + const client = CosmWasmClient.makeReadOnly(httpUrl); + expect(await client.chainId()).toEqual("testing"); + }); + }); + + describe("upload", () => { + it("works", async () => { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { + return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + }); + const codeId = await client.upload(getRandomizedHackatom()); + expect(codeId).toBeGreaterThanOrEqual(1); + }); + }); + + describe("instantiate", () => { + it("works with transfer amount", async () => { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { + return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + }); + const codeId = await client.upload(getRandomizedHackatom()); + + const transferAmount: readonly Coin[] = [ + { + amount: "1234", + denom: "ucosm", + }, + { + amount: "321", + denom: "ustake", + }, + ]; + const beneficiaryAddress = makeRandomAddress(); + const contractAddress = await client.instantiate( + codeId, + { + verifier: faucet.address, + beneficiary: beneficiaryAddress, + }, + "Let's see", + transferAmount, + ); + + const rest = new RestClient(httpUrl); + const balance = (await rest.authAccounts(contractAddress)).result.value.coins; + expect(balance).toEqual(transferAmount); + }); + + it("can instantiate one code multiple times", async () => { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { + return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + }); + const codeId = await client.upload(getRandomizedHackatom()); + + const contractAddress1 = await client.instantiate(codeId, { + verifier: faucet.address, + beneficiary: makeRandomAddress(), + }); + const contractAddress2 = await client.instantiate(codeId, { + verifier: faucet.address, + beneficiary: makeRandomAddress(), + }); + expect(contractAddress1).not.toEqual(contractAddress2); + }); + }); +}); diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts new file mode 100644 index 00000000..acf17400 --- /dev/null +++ b/packages/sdk/src/cosmwasmclient.ts @@ -0,0 +1,147 @@ +import { Encoding } from "@iov/encoding"; + +import { makeSignBytes, marshalTx } from "./encoding"; +import { findAttribute, parseLogs } from "./logs"; +import { RestClient } from "./restclient"; +import { Coin, MsgInstantiateContract, MsgStoreCode, StdFee, StdSignature } from "./types"; + +export interface SigningCallback { + (signBytes: Uint8Array): Promise; +} + +interface SigningData { + readonly senderAddress: string; + readonly signCallback: SigningCallback; +} + +export class CosmWasmClient { + public static makeReadOnly(url: string): CosmWasmClient { + return new CosmWasmClient(url); + } + + public static makeWritable( + url: string, + senderAddress: string, + signCallback: SigningCallback, + ): CosmWasmClient { + return new CosmWasmClient(url, { + senderAddress: senderAddress, + signCallback: signCallback, + }); + } + + private readonly restClient: RestClient; + private readonly signingData: SigningData | undefined; + + private get senderAddress(): string { + if (!this.signingData) throw new Error("Signing data not set in this client"); + return this.signingData.senderAddress; + } + + private get signCallback(): SigningCallback { + if (!this.signingData) throw new Error("Signing data not set in this client"); + return this.signingData.signCallback; + } + + private constructor(url: string, signingData?: SigningData) { + this.restClient = new RestClient(url); + this.signingData = signingData; + } + + public async chainId(): Promise { + const response = await this.restClient.nodeInfo(); + return response.node_info.network; + } + + /** Uploads code and returns a code ID */ + public async upload(wasmCode: Uint8Array, memo?: string): Promise { + const storeCodeMsg: MsgStoreCode = { + type: "wasm/store-code", + value: { + sender: this.senderAddress, + // eslint-disable-next-line @typescript-eslint/camelcase + wasm_byte_code: Encoding.toBase64(wasmCode), + source: "", + builder: "", + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; + const chainId = await this.chainId(); + const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo || "", account); + const signature = await this.signCallback(signBytes); + const signedTx = { + msg: [storeCodeMsg], + fee: fee, + memo: memo || "", + signatures: [signature], + }; + + const result = await this.restClient.postTx(marshalTx(signedTx)); + if (result.code) { + throw new Error(`Error uploading contract. Code: ${result.code}; Raw log: ${result.raw_log}`); + } + const logs = parseLogs(result.logs); + const codeIdAttr = findAttribute(logs, "message", "code_id"); + const codeId = Number.parseInt(codeIdAttr.value, 10); + return codeId; + } + + public async instantiate( + codeId: number, + initMsg: object, + memo?: string, + transferAmount?: readonly Coin[], + ): Promise { + const normalizedMemo = memo || ""; + const instantiateMsg: MsgInstantiateContract = { + type: "wasm/instantiate", + value: { + sender: this.senderAddress, + // eslint-disable-next-line @typescript-eslint/camelcase + code_id: codeId.toString(), + // eslint-disable-next-line @typescript-eslint/camelcase + init_msg: initMsg, + // eslint-disable-next-line @typescript-eslint/camelcase + init_funds: transferAmount || [], + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; + const chainId = await this.chainId(); + const signBytes = makeSignBytes([instantiateMsg], fee, chainId, normalizedMemo, account); + + const signature = await this.signCallback(signBytes); + const signedTx = { + msg: [instantiateMsg], + fee: fee, + memo: normalizedMemo, + signatures: [signature], + }; + const result = await this.restClient.postTx(marshalTx(signedTx)); + if (result.code) { + throw new Error(`Error instantiating contract. Code: ${result.code}; Raw log: ${result.raw_log}`); + } + const logs = parseLogs(result.logs); + const contractAddressAttr = findAttribute(logs, "message", "contract_address"); + return contractAddressAttr.value; + } +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 024cf055..1785b071 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,6 +6,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; +export { CosmWasmClient } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts new file mode 100644 index 00000000..face62a6 --- /dev/null +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -0,0 +1,22 @@ +import { Coin, StdSignature } from "./types"; +export interface SigningCallback { + (signBytes: Uint8Array): Promise; +} +export declare class CosmWasmClient { + static makeReadOnly(url: string): CosmWasmClient; + static makeWritable(url: string, senderAddress: string, signCallback: SigningCallback): CosmWasmClient; + private readonly restClient; + private readonly signingData; + private get senderAddress(); + private get signCallback(); + private constructor(); + chainId(): Promise; + /** Uploads code and returns a code ID */ + upload(wasmCode: Uint8Array, memo?: string): Promise; + instantiate( + codeId: number, + initMsg: object, + memo?: string, + transferAmount?: readonly Coin[], + ): Promise; +} diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 5e0f5db2..e0d05707 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -5,6 +5,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; +export { CosmWasmClient } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, From e7df090ea7bf3d115b8e51865d8461ac6543d171 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 09:35:55 +0100 Subject: [PATCH 04/13] Add CosmWasmClient.execute --- packages/sdk/src/cosmwasmclient.spec.ts | 45 +++++++++++++++++++ packages/sdk/src/cosmwasmclient.ts | 57 ++++++++++++++++++++++++- packages/sdk/types/cosmwasmclient.d.ts | 9 ++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index a513d2f3..10aadfe1 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -109,4 +109,49 @@ describe("CosmWasmClient", () => { expect(contractAddress1).not.toEqual(contractAddress2); }); }); + + describe("execute", () => { + it("works", async () => { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { + return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + }); + const codeId = await client.upload(getRandomizedHackatom()); + + // instantiate + const transferAmount: readonly Coin[] = [ + { + amount: "233444", + denom: "ucosm", + }, + { + amount: "5454", + denom: "ustake", + }, + ]; + const beneficiaryAddress = makeRandomAddress(); + const contractAddress = await client.instantiate( + codeId, + { + verifier: faucet.address, + beneficiary: beneficiaryAddress, + }, + undefined, + transferAmount, + ); + + // execute + const result = await client.execute(contractAddress, {}); + const [firstLog] = result.logs; + expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`); + + // Verify token transfer from contract to beneficiary + const rest = new RestClient(httpUrl); + const beneficiaryBalance = (await rest.authAccounts(beneficiaryAddress)).result.value.coins; + expect(beneficiaryBalance).toEqual(transferAmount); + const contractBalance = (await rest.authAccounts(contractAddress)).result.value.coins; + expect(contractBalance).toEqual([]); + }); + }); }); diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index acf17400..bbdc63e3 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -1,9 +1,16 @@ import { Encoding } from "@iov/encoding"; import { makeSignBytes, marshalTx } from "./encoding"; -import { findAttribute, parseLogs } from "./logs"; +import { findAttribute, Log, parseLogs } from "./logs"; import { RestClient } from "./restclient"; -import { Coin, MsgInstantiateContract, MsgStoreCode, StdFee, StdSignature } from "./types"; +import { + Coin, + MsgExecuteContract, + MsgInstantiateContract, + MsgStoreCode, + StdFee, + StdSignature, +} from "./types"; export interface SigningCallback { (signBytes: Uint8Array): Promise; @@ -144,4 +151,50 @@ export class CosmWasmClient { const contractAddressAttr = findAttribute(logs, "message", "contract_address"); return contractAddressAttr.value; } + + public async execute( + contractAddress: string, + handleMsg: object, + memo?: string, + transferAmount?: readonly Coin[], + ): Promise<{ readonly logs: readonly Log[] }> { + const normalizedMemo = memo || ""; + const executeMsg: MsgExecuteContract = { + type: "wasm/execute", + value: { + sender: this.senderAddress, + contract: contractAddress, + msg: handleMsg, + // eslint-disable-next-line @typescript-eslint/camelcase + sent_funds: transferAmount || [], + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; + const chainId = await this.chainId(); + const signBytes = makeSignBytes([executeMsg], fee, chainId, normalizedMemo, account); + const signature = await this.signCallback(signBytes); + const signedTx = { + msg: [executeMsg], + fee: fee, + memo: normalizedMemo, + signatures: [signature], + }; + const result = await this.restClient.postTx(marshalTx(signedTx)); + if (result.code) { + throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`); + } + return { + logs: parseLogs(result.logs), + }; + } } diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index face62a6..5a07c95f 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -1,3 +1,4 @@ +import { Log } from "./logs"; import { Coin, StdSignature } from "./types"; export interface SigningCallback { (signBytes: Uint8Array): Promise; @@ -19,4 +20,12 @@ export declare class CosmWasmClient { memo?: string, transferAmount?: readonly Coin[], ): Promise; + execute( + contractAddress: string, + handleMsg: object, + memo?: string, + transferAmount?: readonly Coin[], + ): Promise<{ + readonly logs: readonly Log[]; + }>; } From b4b36c3a34273bb77cb86b043471ccf7bfcf8b7d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 09:43:12 +0100 Subject: [PATCH 05/13] Simplify memo handling --- packages/sdk/src/cosmwasmclient.spec.ts | 2 +- packages/sdk/src/cosmwasmclient.ts | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 10aadfe1..30a3686b 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -142,7 +142,7 @@ describe("CosmWasmClient", () => { ); // execute - const result = await client.execute(contractAddress, {}); + const result = await client.execute(contractAddress, {}, undefined); const [firstLog] = result.logs; expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`); diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index bbdc63e3..20e2429c 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -61,7 +61,7 @@ export class CosmWasmClient { } /** Uploads code and returns a code ID */ - public async upload(wasmCode: Uint8Array, memo?: string): Promise { + public async upload(wasmCode: Uint8Array, memo = ""): Promise { const storeCodeMsg: MsgStoreCode = { type: "wasm/store-code", value: { @@ -84,12 +84,12 @@ export class CosmWasmClient { const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; const chainId = await this.chainId(); - const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo || "", account); + const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, account); const signature = await this.signCallback(signBytes); const signedTx = { msg: [storeCodeMsg], fee: fee, - memo: memo || "", + memo: memo, signatures: [signature], }; @@ -106,10 +106,9 @@ export class CosmWasmClient { public async instantiate( codeId: number, initMsg: object, - memo?: string, + memo = "", transferAmount?: readonly Coin[], ): Promise { - const normalizedMemo = memo || ""; const instantiateMsg: MsgInstantiateContract = { type: "wasm/instantiate", value: { @@ -134,13 +133,13 @@ export class CosmWasmClient { const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; const chainId = await this.chainId(); - const signBytes = makeSignBytes([instantiateMsg], fee, chainId, normalizedMemo, account); + const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, account); const signature = await this.signCallback(signBytes); const signedTx = { msg: [instantiateMsg], fee: fee, - memo: normalizedMemo, + memo: memo, signatures: [signature], }; const result = await this.restClient.postTx(marshalTx(signedTx)); @@ -155,10 +154,9 @@ export class CosmWasmClient { public async execute( contractAddress: string, handleMsg: object, - memo?: string, + memo = "", transferAmount?: readonly Coin[], ): Promise<{ readonly logs: readonly Log[] }> { - const normalizedMemo = memo || ""; const executeMsg: MsgExecuteContract = { type: "wasm/execute", value: { @@ -181,12 +179,12 @@ export class CosmWasmClient { const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; const chainId = await this.chainId(); - const signBytes = makeSignBytes([executeMsg], fee, chainId, normalizedMemo, account); + const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, account); const signature = await this.signCallback(signBytes); const signedTx = { msg: [executeMsg], fee: fee, - memo: normalizedMemo, + memo: memo, signatures: [signature], }; const result = await this.restClient.postTx(marshalTx(signedTx)); From ae586cc0127b9019ecc81061e74a903b4e62af26 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 10:26:13 +0100 Subject: [PATCH 06/13] Let makeSignBytes take account number and sequence as two args --- packages/bcp/src/cosmwasmcodec.ts | 8 ++------ packages/bcp/src/cosmwasmconnection.ts | 4 ++-- packages/bcp/src/types.spec.ts | 19 +++---------------- packages/bcp/src/types.ts | 7 +++---- packages/bcp/types/types.d.ts | 3 +-- packages/cli/README.md | 4 ++-- packages/sdk/src/cosmwasmclient.ts | 21 +++++++++++++++------ packages/sdk/src/encoding.ts | 9 +++++---- packages/sdk/src/restclient.spec.ts | 16 ++++++++-------- packages/sdk/src/types.ts | 3 --- packages/sdk/types/encoding.d.ts | 5 +++-- packages/sdk/types/types.d.ts | 2 -- scripts/cosm/deploy_erc20.js | 15 +++++++++++---- 13 files changed, 55 insertions(+), 61 deletions(-) diff --git a/packages/bcp/src/cosmwasmcodec.ts b/packages/bcp/src/cosmwasmcodec.ts index 1b2f1e76..625b285b 100644 --- a/packages/bcp/src/cosmwasmcodec.ts +++ b/packages/bcp/src/cosmwasmcodec.ts @@ -4,7 +4,6 @@ import { isValidAddress, makeSignBytes, marshalTx, - types, unmarshalTx, } from "@cosmwasm/sdk"; import { @@ -46,16 +45,13 @@ export class CosmWasmCodec implements TxCodec { public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob { const built = buildUnsignedTx(unsigned, this.bankTokens, this.erc20Tokens); - const nonceInfo: types.NonceInfo = { - account_number: nonceToAccountNumber(nonce), - sequence: nonceToSequence(nonce), - }; const signBytes = makeSignBytes( built.value.msg, built.value.fee, Caip5.decode(unsigned.chainId), built.value.memo || "", - nonceInfo, + nonceToAccountNumber(nonce), + nonceToSequence(nonce), ); return { diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index fbeedbf2..03d12e2c 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -204,8 +204,8 @@ export class CosmWasmConnection implements BlockchainConnection { public async getNonce(query: AddressQuery | PubkeyQuery): Promise { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address; const { result } = await this.restClient.authAccounts(address); - const account = result.value; - return accountToNonce(account); + const { account_number, sequence } = result.value; + return accountToNonce(account_number, sequence); } public async getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise { diff --git a/packages/bcp/src/types.spec.ts b/packages/bcp/src/types.spec.ts index 3fab0b47..a5025437 100644 --- a/packages/bcp/src/types.spec.ts +++ b/packages/bcp/src/types.spec.ts @@ -3,26 +3,13 @@ import { accountToNonce, nonceToAccountNumber, nonceToSequence } from "./types"; describe("nonceEncoding", () => { it("works for input in range", () => { - const nonce = accountToNonce({ - account_number: 1234, - sequence: 7890, - }); + const nonce = accountToNonce(1234, 7890); expect(nonceToAccountNumber(nonce)).toEqual(1234); expect(nonceToSequence(nonce)).toEqual(7890); }); it("errors on input too large", () => { - expect(() => - accountToNonce({ - account_number: 1234567890, - sequence: 7890, - }), - ).toThrow(); - expect(() => - accountToNonce({ - account_number: 178, - sequence: 97320247923, - }), - ).toThrow(); + expect(() => accountToNonce(1234567890, 7890)).toThrow(); + expect(() => accountToNonce(178, 97320247923)).toThrow(); }); }); diff --git a/packages/bcp/src/types.ts b/packages/bcp/src/types.ts index ae679136..a9fdad3a 100644 --- a/packages/bcp/src/types.ts +++ b/packages/bcp/src/types.ts @@ -1,4 +1,3 @@ -import { types } from "@cosmwasm/sdk"; import { Nonce } from "@iov/bcp"; export interface BankToken { @@ -40,17 +39,17 @@ const maxSeq = 1 << 20; // this (lossily) encodes the two pieces of info (uint64) needed to sign into // one (53-bit) number. Cross your fingers. -export function accountToNonce({ account_number: account, sequence }: types.NonceInfo): Nonce { +export function accountToNonce(accountNumber: number, sequence: number): Nonce { // we allow 23 bits (8 million) for accounts, and 20 bits (1 million) for tx/account // let's fix this soon - if (account > maxAcct) { + if (accountNumber > maxAcct) { throw new Error("Account number is greater than 2^23, must update Nonce handler"); } if (sequence > maxSeq) { throw new Error("Sequence is greater than 2^20, must update Nonce handler"); } - const val = account * maxSeq + sequence; + const val = accountNumber * maxSeq + sequence; return val as Nonce; } diff --git a/packages/bcp/types/types.d.ts b/packages/bcp/types/types.d.ts index 35863a03..e4777786 100644 --- a/packages/bcp/types/types.d.ts +++ b/packages/bcp/types/types.d.ts @@ -1,4 +1,3 @@ -import { types } from "@cosmwasm/sdk"; import { Nonce } from "@iov/bcp"; export interface BankToken { readonly denom: string; @@ -29,6 +28,6 @@ export interface Erc20Token { */ readonly fractionalDigits: number; } -export declare function accountToNonce({ account_number: account, sequence }: types.NonceInfo): Nonce; +export declare function accountToNonce(accountNumber: number, sequence: number): Nonce; export declare function nonceToAccountNumber(nonce: Nonce): number; export declare function nonceToSequence(nonce: Nonce): number; diff --git a/packages/cli/README.md b/packages/cli/README.md index e4f8ed3c..cc0104c9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -45,7 +45,7 @@ $ cosmwasm-cli ```ts // Get account information -const account = (await client.authAccounts(faucetAddress)).result.value; +const { account_number, sequence } = (await client.authAccounts(faucetAddress)).result.value; // Craft a send transaction const emptyAddress = Bech32.encode("cosmos", Random.getBytes(20)); @@ -64,7 +64,7 @@ const sendTokensMsg: types.MsgSend = { }, }; -const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account); +const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account_number, sequence); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx: types.StdTx = { msg: [sendTokensMsg], diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 20e2429c..eaa72bd8 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -82,9 +82,12 @@ export class CosmWasmClient { gas: "89000000", }; - const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; + // eslint-disable-next-line @typescript-eslint/camelcase + const { account_number, sequence } = ( + await this.restClient.authAccounts(this.senderAddress) + ).result.value; const chainId = await this.chainId(); - const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, account); + const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, account_number, sequence); const signature = await this.signCallback(signBytes); const signedTx = { msg: [storeCodeMsg], @@ -131,9 +134,12 @@ export class CosmWasmClient { gas: "89000000", }; - const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; + // eslint-disable-next-line @typescript-eslint/camelcase + const { account_number, sequence } = ( + await this.restClient.authAccounts(this.senderAddress) + ).result.value; const chainId = await this.chainId(); - const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, account); + const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, account_number, sequence); const signature = await this.signCallback(signBytes); const signedTx = { @@ -177,9 +183,12 @@ export class CosmWasmClient { gas: "89000000", }; - const account = (await this.restClient.authAccounts(this.senderAddress)).result.value; + // eslint-disable-next-line @typescript-eslint/camelcase + const { account_number, sequence } = ( + await this.restClient.authAccounts(this.senderAddress) + ).result.value; const chainId = await this.chainId(); - const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, account); + const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, account_number, sequence); const signature = await this.signCallback(signBytes); const signedTx = { msg: [executeMsg], diff --git a/packages/sdk/src/encoding.ts b/packages/sdk/src/encoding.ts index 49a9fde4..c8045b36 100644 --- a/packages/sdk/src/encoding.ts +++ b/packages/sdk/src/encoding.ts @@ -2,7 +2,7 @@ import { Secp256k1 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import { encodeSecp256k1Pubkey } from "./pubkey"; -import { Msg, NonceInfo, StdFee, StdSignature, StdTx } from "./types"; +import { Msg, StdFee, StdSignature, StdTx } from "./types"; const { toBase64, toUtf8 } = Encoding; @@ -43,17 +43,18 @@ export function makeSignBytes( fee: StdFee, chainId: string, memo: string, - account: NonceInfo, + accountNumber: number, + sequence: number, ): Uint8Array { const signJson: SignJson = { // eslint-disable-next-line @typescript-eslint/camelcase - account_number: account.account_number.toString(), + account_number: accountNumber.toString(), // eslint-disable-next-line @typescript-eslint/camelcase chain_id: chainId, fee: fee, memo: memo, msgs: msgs, - sequence: account.sequence.toString(), + sequence: sequence.toString(), }; const signMsg = sortJson(signJson); return toUtf8(JSON.stringify(signMsg)); diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index c0930d07..26a654b7 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -84,8 +84,8 @@ async function uploadCustomContract( gas: "89000000", }; - const account = (await client.authAccounts(faucet.address)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account); + const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx = makeSignedTx(theMsg, fee, memo, signature); return client.postTx(marshalTx(signedTx)); @@ -125,8 +125,8 @@ async function instantiateContract( gas: "89000000", }; - const account = (await client.authAccounts(faucet.address)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account); + const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx = makeSignedTx(theMsg, fee, memo, signature); return client.postTx(marshalTx(signedTx)); @@ -157,8 +157,8 @@ async function executeContract( gas: "89000000", }; - const account = (await client.authAccounts(faucet.address)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account); + const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx = makeSignedTx(theMsg, fee, memo, signature); return client.postTx(marshalTx(signedTx)); @@ -258,9 +258,9 @@ describe("RestClient", () => { }; const client = new RestClient(httpUrl); - const account = (await client.authAccounts(faucet.address)).result.value; + const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account); + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx = makeSignedTx(theMsg, fee, memo, signature); const result = await client.postTx(marshalTx(signedTx)); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 9ec7c3d3..75ab00a5 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -160,9 +160,6 @@ export interface CosmosSdkAccount { readonly sequence: number; } -/** The data we need from CosmosSdkAccount to create a nonce */ -export type NonceInfo = Pick; - export interface CodeInfo { readonly id: number; /** Bech32 account address */ diff --git a/packages/sdk/types/encoding.d.ts b/packages/sdk/types/encoding.d.ts index 2ed63061..e4a6c77f 100644 --- a/packages/sdk/types/encoding.d.ts +++ b/packages/sdk/types/encoding.d.ts @@ -1,10 +1,11 @@ -import { Msg, NonceInfo, StdFee, StdSignature, StdTx } from "./types"; +import { Msg, StdFee, StdSignature, StdTx } from "./types"; export declare function marshalTx(tx: StdTx): Uint8Array; export declare function makeSignBytes( msgs: readonly Msg[], fee: StdFee, chainId: string, memo: string, - account: NonceInfo, + accountNumber: number, + sequence: number, ): Uint8Array; export declare function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index af31396b..2cf95184 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -116,8 +116,6 @@ export interface CosmosSdkAccount { readonly account_number: number; readonly sequence: number; } -/** The data we need from CosmosSdkAccount to create a nonce */ -export declare type NonceInfo = Pick; export interface CodeInfo { readonly id: number; /** Bech32 account address */ diff --git a/scripts/cosm/deploy_erc20.js b/scripts/cosm/deploy_erc20.js index 78901bf1..46f85106 100755 --- a/scripts/cosm/deploy_erc20.js +++ b/scripts/cosm/deploy_erc20.js @@ -40,8 +40,8 @@ async function uploadContract(client, pen, wasm) { }, }; - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([storeCodeMsg], defaultFee, networkId, memo, account); + const { account_number, sequence } = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([storeCodeMsg], defaultFee, networkId, memo, account_number, sequence); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx = { msg: [storeCodeMsg], @@ -63,8 +63,15 @@ async function instantiateContract(client, pen, codeId, msg, transferAmount) { init_funds: transferAmount || [], }, }; - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([instantiateContractMsg], defaultFee, networkId, memo, account); + const { account_number, sequence } = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes( + [instantiateContractMsg], + defaultFee, + networkId, + memo, + account_number, + sequence, + ); const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); const signedTx = { msg: [instantiateContractMsg], From 0e0ff9455f904deeae5d3805004225f373359f71 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 10:35:29 +0100 Subject: [PATCH 07/13] Add and use CosmWasmClient.getNonce --- packages/sdk/src/cosmwasmclient.spec.ts | 15 ++++++++++ packages/sdk/src/cosmwasmclient.ts | 39 +++++++++++++++---------- packages/sdk/src/index.ts | 2 +- packages/sdk/types/cosmwasmclient.d.ts | 10 +++++++ packages/sdk/types/index.d.ts | 2 +- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 30a3686b..9c6e0b5c 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -27,6 +27,10 @@ const faucet = { address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", }; +const unusedAccount = { + address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u", +}; + describe("CosmWasmClient", () => { describe("makeReadOnly", () => { it("can be constructed", () => { @@ -43,6 +47,17 @@ describe("CosmWasmClient", () => { }); }); + describe("getNonce", () => { + it("works", async () => { + pendingWithoutCosmos(); + const client = CosmWasmClient.makeReadOnly(httpUrl); + expect(await client.getNonce(unusedAccount.address)).toEqual({ + accountNumber: 5, + sequence: 0, + }); + }); + }); + describe("upload", () => { it("works", async () => { pendingWithoutCosmos(); diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index eaa72bd8..2e32ddea 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -21,6 +21,11 @@ interface SigningData { readonly signCallback: SigningCallback; } +export interface GetNonceResult { + readonly accountNumber: number; + readonly sequence: number; +} + export class CosmWasmClient { public static makeReadOnly(url: string): CosmWasmClient { return new CosmWasmClient(url); @@ -60,6 +65,19 @@ export class CosmWasmClient { return response.node_info.network; } + /** + * Returns account number and sequence. + * + * @param address returns data for this address. When unset, the client's sender adddress is used. + */ + public async getNonce(address?: string): Promise { + const account = (await this.restClient.authAccounts(address || this.senderAddress)).result.value; + return { + accountNumber: account.account_number, + sequence: account.sequence, + }; + } + /** Uploads code and returns a code ID */ public async upload(wasmCode: Uint8Array, memo = ""): Promise { const storeCodeMsg: MsgStoreCode = { @@ -82,12 +100,9 @@ export class CosmWasmClient { gas: "89000000", }; - // eslint-disable-next-line @typescript-eslint/camelcase - const { account_number, sequence } = ( - await this.restClient.authAccounts(this.senderAddress) - ).result.value; + const { accountNumber, sequence } = await this.getNonce(); const chainId = await this.chainId(); - const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, account_number, sequence); + const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, accountNumber, sequence); const signature = await this.signCallback(signBytes); const signedTx = { msg: [storeCodeMsg], @@ -134,12 +149,9 @@ export class CosmWasmClient { gas: "89000000", }; - // eslint-disable-next-line @typescript-eslint/camelcase - const { account_number, sequence } = ( - await this.restClient.authAccounts(this.senderAddress) - ).result.value; + const { accountNumber, sequence } = await this.getNonce(); const chainId = await this.chainId(); - const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, account_number, sequence); + const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, accountNumber, sequence); const signature = await this.signCallback(signBytes); const signedTx = { @@ -183,12 +195,9 @@ export class CosmWasmClient { gas: "89000000", }; - // eslint-disable-next-line @typescript-eslint/camelcase - const { account_number, sequence } = ( - await this.restClient.authAccounts(this.senderAddress) - ).result.value; + const { accountNumber, sequence } = await this.getNonce(); const chainId = await this.chainId(); - const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, account_number, sequence); + const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, accountNumber, sequence); const signature = await this.signCallback(signBytes); const signedTx = { msg: [executeMsg], diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 1785b071..abfadd1a 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,7 +6,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { CosmWasmClient } from "./cosmwasmclient"; +export { CosmWasmClient, GetNonceResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index 5a07c95f..dac30039 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -3,6 +3,10 @@ import { Coin, StdSignature } from "./types"; export interface SigningCallback { (signBytes: Uint8Array): Promise; } +export interface GetNonceResult { + readonly accountNumber: number; + readonly sequence: number; +} export declare class CosmWasmClient { static makeReadOnly(url: string): CosmWasmClient; static makeWritable(url: string, senderAddress: string, signCallback: SigningCallback): CosmWasmClient; @@ -12,6 +16,12 @@ export declare class CosmWasmClient { private get signCallback(); private constructor(); chainId(): Promise; + /** + * Returns account number and sequence. + * + * @param address returns data for this address. When unset, the client's sender adddress is used. + */ + getNonce(address?: string): Promise; /** Uploads code and returns a code ID */ upload(wasmCode: Uint8Array, memo?: string): Promise; instantiate( diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index e0d05707..67cdb1c0 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -5,7 +5,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { CosmWasmClient } from "./cosmwasmclient"; +export { CosmWasmClient, GetNonceResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, From 83bb1021aaa928acf2bf258c94bc82208389ba19 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 10:48:50 +0100 Subject: [PATCH 08/13] Add CosmWasmClient.postTx --- packages/sdk/src/cosmwasmclient.spec.ts | 54 ++++++++++++++++++++++++- packages/sdk/src/cosmwasmclient.ts | 43 ++++++++++++-------- packages/sdk/src/index.ts | 2 +- packages/sdk/types/cosmwasmclient.d.ts | 6 +++ packages/sdk/types/index.d.ts | 2 +- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 9c6e0b5c..5aa92931 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -1,9 +1,10 @@ import { CosmWasmClient } from "./cosmwasmclient"; -import { encodeSecp256k1Signature } from "./encoding"; +import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; +import { findAttribute } from "./logs"; import { Secp256k1Pen } from "./pen"; import { RestClient } from "./restclient"; import { getRandomizedHackatom, makeRandomAddress } from "./testutils.spec"; -import { Coin } from "./types"; +import { Coin, MsgSend, StdFee } from "./types"; const httpUrl = "http://localhost:1317"; @@ -58,6 +59,55 @@ describe("CosmWasmClient", () => { }); }); + describe("postTx", () => { + it("works", async () => { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeReadOnly(httpUrl); + + const memo = "My first contract on chain"; + const sendMsg: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + // eslint-disable-next-line @typescript-eslint/camelcase + from_address: faucet.address, + // eslint-disable-next-line @typescript-eslint/camelcase + to_address: makeRandomAddress(), + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }; + + const fee: StdFee = { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "890000", + }; + + const chainId = await client.chainId(); + const { accountNumber, sequence } = await client.getNonce(faucet.address); + const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); + const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + const signedTx = { + msg: [sendMsg], + fee: fee, + memo: memo, + signatures: [signature], + }; + const { logs } = await client.postTx(marshalTx(signedTx)); + const amountAttr = findAttribute(logs, "transfer", "amount"); + expect(amountAttr.value).toEqual("1234567ucosm"); + }); + }); + describe("upload", () => { it("works", async () => { pendingWithoutCosmos(); diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 2e32ddea..941d341e 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -26,6 +26,12 @@ export interface GetNonceResult { readonly sequence: number; } +export interface PostTxResult { + readonly logs: readonly Log[]; + readonly rawLog: string; + readonly transactionHash: string; +} + export class CosmWasmClient { public static makeReadOnly(url: string): CosmWasmClient { return new CosmWasmClient(url); @@ -78,6 +84,18 @@ export class CosmWasmClient { }; } + public async postTx(tx: Uint8Array): Promise { + const result = await this.restClient.postTx(tx); + if (result.code) { + throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`); + } + return { + logs: parseLogs(result.logs) || [], + rawLog: result.raw_log || "", + transactionHash: result.txhash, + }; + } + /** Uploads code and returns a code ID */ public async upload(wasmCode: Uint8Array, memo = ""): Promise { const storeCodeMsg: MsgStoreCode = { @@ -111,12 +129,8 @@ export class CosmWasmClient { signatures: [signature], }; - const result = await this.restClient.postTx(marshalTx(signedTx)); - if (result.code) { - throw new Error(`Error uploading contract. Code: ${result.code}; Raw log: ${result.raw_log}`); - } - const logs = parseLogs(result.logs); - const codeIdAttr = findAttribute(logs, "message", "code_id"); + const result = await this.postTx(marshalTx(signedTx)); + const codeIdAttr = findAttribute(result.logs, "message", "code_id"); const codeId = Number.parseInt(codeIdAttr.value, 10); return codeId; } @@ -160,12 +174,9 @@ export class CosmWasmClient { memo: memo, signatures: [signature], }; - const result = await this.restClient.postTx(marshalTx(signedTx)); - if (result.code) { - throw new Error(`Error instantiating contract. Code: ${result.code}; Raw log: ${result.raw_log}`); - } - const logs = parseLogs(result.logs); - const contractAddressAttr = findAttribute(logs, "message", "contract_address"); + + const result = await this.postTx(marshalTx(signedTx)); + const contractAddressAttr = findAttribute(result.logs, "message", "contract_address"); return contractAddressAttr.value; } @@ -205,12 +216,10 @@ export class CosmWasmClient { memo: memo, signatures: [signature], }; - const result = await this.restClient.postTx(marshalTx(signedTx)); - if (result.code) { - throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`); - } + + const result = await this.postTx(marshalTx(signedTx)); return { - logs: parseLogs(result.logs), + logs: result.logs, }; } } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index abfadd1a..b574af12 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,7 +6,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { CosmWasmClient, GetNonceResult } from "./cosmwasmclient"; +export { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index dac30039..be4c4222 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -7,6 +7,11 @@ export interface GetNonceResult { readonly accountNumber: number; readonly sequence: number; } +export interface PostTxResult { + readonly logs: readonly Log[]; + readonly rawLog: string; + readonly transactionHash: string; +} export declare class CosmWasmClient { static makeReadOnly(url: string): CosmWasmClient; static makeWritable(url: string, senderAddress: string, signCallback: SigningCallback): CosmWasmClient; @@ -22,6 +27,7 @@ export declare class CosmWasmClient { * @param address returns data for this address. When unset, the client's sender adddress is used. */ getNonce(address?: string): Promise; + postTx(tx: Uint8Array): Promise; /** Uploads code and returns a code ID */ upload(wasmCode: Uint8Array, memo?: string): Promise; instantiate( diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 67cdb1c0..7e9834b8 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -5,7 +5,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { CosmWasmClient, GetNonceResult } from "./cosmwasmclient"; +export { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, From 6783b273f89a9b1004d6957b50a3fcf6c4d2d507 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 11:09:22 +0100 Subject: [PATCH 09/13] Pull out explicit fee constants --- packages/sdk/src/cosmwasmclient.ts | 63 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 941d341e..2eb9985a 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -12,6 +12,36 @@ import { StdSignature, } from "./types"; +const defaultUploadFee: StdFee = { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "1000000", // one million +}; + +const defaultInitFee: StdFee = { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "500000", // 500k +}; + +const defaultExecFee: StdFee = { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "200000", // 200k +}; + export interface SigningCallback { (signBytes: Uint8Array): Promise; } @@ -108,16 +138,7 @@ export class CosmWasmClient { builder: "", }, }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - + const fee = defaultUploadFee; const { accountNumber, sequence } = await this.getNonce(); const chainId = await this.chainId(); const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, accountNumber, sequence); @@ -153,16 +174,7 @@ export class CosmWasmClient { init_funds: transferAmount || [], }, }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - + const fee = defaultInitFee; const { accountNumber, sequence } = await this.getNonce(); const chainId = await this.chainId(); const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, accountNumber, sequence); @@ -196,16 +208,7 @@ export class CosmWasmClient { sent_funds: transferAmount || [], }, }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - + const fee = defaultExecFee; const { accountNumber, sequence } = await this.getNonce(); const chainId = await this.chainId(); const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, accountNumber, sequence); From 225cde411355f3743c9cd2a1c97363199aaa9762 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 11:19:04 +0100 Subject: [PATCH 10/13] Use CosmWasmClient to deploy contracts --- scripts/cosm/deploy_erc20.js | 178 ++++++++++------------------------- 1 file changed, 51 insertions(+), 127 deletions(-) diff --git a/scripts/cosm/deploy_erc20.js b/scripts/cosm/deploy_erc20.js index 46f85106..94ed3522 100755 --- a/scripts/cosm/deploy_erc20.js +++ b/scripts/cosm/deploy_erc20.js @@ -1,148 +1,72 @@ #!/usr/bin/env node /* eslint-disable @typescript-eslint/camelcase */ -const { Encoding } = require("@iov/encoding"); -const { - encodeSecp256k1Signature, - makeSignBytes, - marshalTx, - logs, - RestClient, - Secp256k1Pen, -} = require("@cosmwasm/sdk"); +const { CosmWasmClient, encodeSecp256k1Signature, Secp256k1Pen } = require("@cosmwasm/sdk"); const fs = require("fs"); const httpUrl = "http://localhost:1317"; -const networkId = "testing"; -const defaultFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - gas: "1000000", // one million +const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", }; -const faucetMnemonic = - "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone"; -const faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"; const unusedAccount = "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u"; -async function uploadContract(client, pen, wasm) { - const memo = "Upload ERC20 contract"; - const storeCodeMsg = { - type: "wasm/store-code", - value: { - sender: faucetAddress, - wasm_byte_code: Encoding.toBase64(wasm), - source: "", - builder: "", +const initMsgAsh = { + decimals: 5, + name: "Ash token", + symbol: "ASH", + initial_balances: [ + { + address: faucet.address, + amount: "11", }, - }; - - const { account_number, sequence } = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([storeCodeMsg], defaultFee, networkId, memo, account_number, sequence); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - const signedTx = { - msg: [storeCodeMsg], - fee: defaultFee, - memo: memo, - signatures: [signature], - }; - return client.postTx(marshalTx(signedTx)); -} - -async function instantiateContract(client, pen, codeId, msg, transferAmount) { - const memo = "Create an ERC20 instance"; - const instantiateContractMsg = { - type: "wasm/instantiate", - value: { - sender: faucetAddress, - code_id: codeId.toString(), - init_msg: msg, - init_funds: transferAmount || [], + { + address: unusedAccount, + amount: "12812345", }, - }; - const { account_number, sequence } = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes( - [instantiateContractMsg], - defaultFee, - networkId, - memo, - account_number, - sequence, - ); - const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); - const signedTx = { - msg: [instantiateContractMsg], - fee: defaultFee, - memo: memo, - signatures: [signature], - }; - return client.postTx(marshalTx(signedTx)); -} + ], +}; +const initMsgBash = { + decimals: 0, + name: "Bash Token", + symbol: "BASH", + initial_balances: [ + { + address: faucet.address, + amount: "999999999", + }, + { + address: unusedAccount, + amount: "42", + }, + ], +}; +const initMsgCash = { + decimals: 18, + name: "Cash Token", + symbol: "CASH", + initial_balances: [ + { + address: faucet.address, + amount: "189189189000000000000000000", // 189189189 CASH + }, + ], +}; async function main() { - const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic); - const client = new RestClient(httpUrl); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => { + return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes)); + }); const wasm = fs.readFileSync(__dirname + "/contracts/cw-erc20.wasm"); - const uploadResult = await uploadContract(client, pen, wasm); - if (uploadResult.code) { - throw new Error(`Uploading failed with code: ${uploadResult.code}; log: '${uploadResult.raw_log}'`); - } - const codeIdAttr = logs.findAttribute(logs.parseLogs(uploadResult.logs), "message", "code_id"); - const codeId = Number.parseInt(codeIdAttr.value, 10); + const codeId = await client.upload(wasm, "Upload ERC20 contract"); console.info(`Upload succeeded. Code ID is ${codeId}`); - const initMsgAsh = { - decimals: 5, - name: "Ash token", - symbol: "ASH", - initial_balances: [ - { - address: faucetAddress, - amount: "11", - }, - { - address: unusedAccount, - amount: "12812345", - }, - ], - }; - const initMsgBash = { - decimals: 0, - name: "Bash Token", - symbol: "BASH", - initial_balances: [ - { - address: faucetAddress, - amount: "999999999", - }, - { - address: unusedAccount, - amount: "42", - }, - ], - }; - const initMsgCash = { - decimals: 18, - name: "Cash Token", - symbol: "CASH", - initial_balances: [ - { - address: faucetAddress, - amount: "189189189000000000000000000", // 189189189 CASH - }, - ], - }; for (const initMsg of [initMsgAsh, initMsgBash, initMsgCash]) { - const initResult = await instantiateContract(client, pen, codeId, initMsg); - if (initResult.code) { - throw new Error(`Instantiation failed with code: ${initResult.code}; log: '${initResult.raw_log}'`); - } - const instantiationLogs = logs.parseLogs(initResult.logs); - const contractAddress = logs.findAttribute(instantiationLogs, "message", "contract_address").value; + const memo = `Create an ERC20 instance for ${initMsg.symbol}`; + const contractAddress = await client.instantiate(codeId, initMsg, memo); console.info(`Contract instantiated for ${initMsg.symbol} at ${contractAddress}`); } } From 8c68e66b9736166fdd93af2f7e06f339057cea9d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 11:36:06 +0100 Subject: [PATCH 11/13] Use CosmWasmClient in CosmWasmConnection --- packages/bcp/src/cosmwasmconnection.ts | 47 +++++++++++++++------- packages/bcp/types/cosmwasmconnection.d.ts | 2 + 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 03d12e2c..a659d89f 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -1,5 +1,12 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { CosmosAddressBech32Prefix, RestClient, TxsResponse, types, unmarshalTx } from "@cosmwasm/sdk"; +import { + CosmosAddressBech32Prefix, + CosmWasmClient, + RestClient, + TxsResponse, + types, + unmarshalTx, +} from "@cosmwasm/sdk"; import { Account, AccountQuery, @@ -86,16 +93,19 @@ export class CosmWasmConnection implements BlockchainConnection { tokens: TokenConfiguration, ): Promise { const restClient = new RestClient(url); - const chainData = await this.initialize(restClient); - return new CosmWasmConnection(restClient, chainData, addressPrefix, tokens); + const cosmWasmClient = CosmWasmClient.makeReadOnly(url); + const chainData = await this.initialize(cosmWasmClient); + return new CosmWasmConnection(restClient, cosmWasmClient, chainData, addressPrefix, tokens); } - private static async initialize(restClient: RestClient): Promise { - const { node_info } = await restClient.nodeInfo(); - return { chainId: Caip5.encode(node_info.network) }; + private static async initialize(cosmWasmClient: CosmWasmClient): Promise { + const rawChainId = await cosmWasmClient.chainId(); + return { chainId: Caip5.encode(rawChainId) }; } + /** @deprecated everything we use from RestClient should be available in CosmWasmClient */ private readonly restClient: RestClient; + private readonly cosmWasmClient: CosmWasmClient; private readonly chainData: ChainData; private readonly addressPrefix: CosmosAddressBech32Prefix; private readonly bankTokens: readonly BankToken[]; @@ -107,11 +117,14 @@ export class CosmWasmConnection implements BlockchainConnection { private constructor( restClient: RestClient, + cosmWasmClient: CosmWasmClient, chainData: ChainData, addressPrefix: CosmosAddressBech32Prefix, tokens: TokenConfiguration, ) { + // tslint:disable-next-line: deprecation this.restClient = restClient; + this.cosmWasmClient = cosmWasmClient; this.chainData = chainData; this.addressPrefix = addressPrefix; this.bankTokens = tokens.bankTokens; @@ -136,6 +149,7 @@ export class CosmWasmConnection implements BlockchainConnection { } public async height(): Promise { + // tslint:disable-next-line: deprecation const { block } = await this.restClient.blocksLatest(); return block.header.height; } @@ -150,6 +164,7 @@ export class CosmWasmConnection implements BlockchainConnection { public async identifier(signed: PostableBytes): Promise { const tx = unmarshalTx(signed); + // tslint:disable-next-line: deprecation const bytes = await this.restClient.encodeTx(tx); const hash = new Sha256(bytes).digest(); return toHex(hash).toUpperCase() as TransactionId; @@ -157,6 +172,7 @@ export class CosmWasmConnection implements BlockchainConnection { public async getAccount(query: AccountQuery): Promise { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address; + // tslint:disable-next-line: deprecation const { result } = await this.restClient.authAccounts(address); const bankAccount = result.value; const hasBankAccount = !!bankAccount.address; @@ -168,6 +184,7 @@ export class CosmWasmConnection implements BlockchainConnection { this.erc20Tokens.map( async (erc20): Promise => { const queryMsg = { balance: { address: address } }; + // tslint:disable-next-line: deprecation const smart = await this.restClient.queryContractSmart(erc20.contractAddress, queryMsg); const response = JSON.parse(fromAscii(smart)); const normalizedBalance = new BN(response.balance).toString(); @@ -203,9 +220,8 @@ export class CosmWasmConnection implements BlockchainConnection { public async getNonce(query: AddressQuery | PubkeyQuery): Promise { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address; - const { result } = await this.restClient.authAccounts(address); - const { account_number, sequence } = result.value; - return accountToNonce(account_number, sequence); + const { accountNumber, sequence } = await this.cosmWasmClient.getNonce(address); + return accountToNonce(accountNumber, sequence); } public async getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise { @@ -219,6 +235,7 @@ export class CosmWasmConnection implements BlockchainConnection { } public async getBlockHeader(height: number): Promise { + // tslint:disable-next-line: deprecation const { block_meta } = await this.restClient.blocks(height); return { id: block_meta.block_id.hash as BlockId, @@ -236,6 +253,7 @@ export class CosmWasmConnection implements BlockchainConnection { id: TransactionId, ): Promise | FailedTransaction> { try { + // tslint:disable-next-line: deprecation const response = await this.restClient.txsById(id); const chainId = this.chainId(); return this.parseAndPopulateTxResponse(response, chainId); @@ -248,11 +266,8 @@ export class CosmWasmConnection implements BlockchainConnection { } public async postTx(tx: PostableBytes): Promise { - const { code, txhash, raw_log } = await this.restClient.postTx(tx); - if (code) { - throw new Error(raw_log); - } - const transactionId = txhash as TransactionId; + const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(tx); + const transactionId = transactionHash as TransactionId; const firstEvent: BlockInfo = { state: TransactionState.Pending }; let blockInfoInterval: NodeJS.Timeout; let lastEventSent: BlockInfo; @@ -285,7 +300,7 @@ export class CosmWasmConnection implements BlockchainConnection { return { blockInfo: new ValueAndUpdates(producer), transactionId: transactionId, - log: raw_log, + log: rawLog, }; } @@ -295,6 +310,7 @@ export class CosmWasmConnection implements BlockchainConnection { const queryString = buildQueryString(query); const chainId = this.chainId(); // TODO: we need pagination support + // tslint:disable-next-line: deprecation const response = await this.restClient.txs(queryString + "&limit=50"); const { txs } = response; return Promise.all(txs.map(tx => this.parseAndPopulateTxResponse(tx, chainId))); @@ -354,6 +370,7 @@ export class CosmWasmConnection implements BlockchainConnection { throw new Error(`Got unsupported type of message: ${firstMsg.type}`); } + // tslint:disable-next-line: deprecation const accountForHeight = await this.restClient.authAccounts(senderAddress, response.height); // this is technically not the proper nonce. maybe this causes issues for sig validation? // leaving for now unless it causes issues diff --git a/packages/bcp/types/cosmwasmconnection.d.ts b/packages/bcp/types/cosmwasmconnection.d.ts index 5b98a5fa..6d15edea 100644 --- a/packages/bcp/types/cosmwasmconnection.d.ts +++ b/packages/bcp/types/cosmwasmconnection.d.ts @@ -43,7 +43,9 @@ export declare class CosmWasmConnection implements BlockchainConnection { tokens: TokenConfiguration, ): Promise; private static initialize; + /** @deprecated everything we use from RestClient should be available in CosmWasmClient */ private readonly restClient; + private readonly cosmWasmClient; private readonly chainData; private readonly addressPrefix; private readonly bankTokens; From 90e76b7a41996c459a9665dda049d22918bc4565 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 14:32:31 +0100 Subject: [PATCH 12/13] Create type ExecuteResult --- packages/sdk/src/cosmwasmclient.ts | 6 +++++- packages/sdk/src/index.ts | 2 +- packages/sdk/types/cosmwasmclient.d.ts | 7 ++++--- packages/sdk/types/index.d.ts | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 2eb9985a..876be019 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -62,6 +62,10 @@ export interface PostTxResult { readonly transactionHash: string; } +export interface ExecuteResult { + readonly logs: readonly Log[]; +} + export class CosmWasmClient { public static makeReadOnly(url: string): CosmWasmClient { return new CosmWasmClient(url); @@ -197,7 +201,7 @@ export class CosmWasmClient { handleMsg: object, memo = "", transferAmount?: readonly Coin[], - ): Promise<{ readonly logs: readonly Log[] }> { + ): Promise { const executeMsg: MsgExecuteContract = { type: "wasm/execute", value: { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index b574af12..a3331203 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,7 +6,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; +export { CosmWasmClient, ExecuteResult, GetNonceResult, PostTxResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index be4c4222..18cc8016 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -12,6 +12,9 @@ export interface PostTxResult { readonly rawLog: string; readonly transactionHash: string; } +export interface ExecuteResult { + readonly logs: readonly Log[]; +} export declare class CosmWasmClient { static makeReadOnly(url: string): CosmWasmClient; static makeWritable(url: string, senderAddress: string, signCallback: SigningCallback): CosmWasmClient; @@ -41,7 +44,5 @@ export declare class CosmWasmClient { handleMsg: object, memo?: string, transferAmount?: readonly Coin[], - ): Promise<{ - readonly logs: readonly Log[]; - }>; + ): Promise; } diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 7e9834b8..9250b24c 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -5,7 +5,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; +export { CosmWasmClient, ExecuteResult, GetNonceResult, PostTxResult } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { CosmosPubkeyBech32Prefix, From 4c7124d12c9da060654ae1ab118451b705a8e745 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 16:41:02 +0100 Subject: [PATCH 13/13] Add format guarantee to PostTxResult.transactionHash --- packages/sdk/src/cosmwasmclient.spec.ts | 3 ++- packages/sdk/src/cosmwasmclient.ts | 6 ++++++ packages/sdk/types/cosmwasmclient.d.ts | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 5aa92931..740b00aa 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -102,9 +102,10 @@ describe("CosmWasmClient", () => { memo: memo, signatures: [signature], }; - const { logs } = await client.postTx(marshalTx(signedTx)); + const { logs, transactionHash } = await client.postTx(marshalTx(signedTx)); const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234567ucosm"); + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); }); }); diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 876be019..f1c191e7 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -59,6 +59,7 @@ export interface GetNonceResult { export interface PostTxResult { readonly logs: readonly Log[]; readonly rawLog: string; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-exmpty upper-case hex */ readonly transactionHash: string; } @@ -123,6 +124,11 @@ export class CosmWasmClient { if (result.code) { throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`); } + + if (!result.txhash.match(/^([0-9A-F][0-9A-F])+$/)) { + throw new Error("Received ill-formatted txhash. Must be non-empty upper-case hex"); + } + return { logs: parseLogs(result.logs) || [], rawLog: result.raw_log || "", diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index 18cc8016..bee7e6a2 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -10,6 +10,7 @@ export interface GetNonceResult { export interface PostTxResult { readonly logs: readonly Log[]; readonly rawLog: string; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-exmpty upper-case hex */ readonly transactionHash: string; } export interface ExecuteResult {