From 66fd0800ff6e26b008dad54d1168c964ab2fd3b6 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 11 Feb 2020 00:49:59 +0100 Subject: [PATCH] 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,