Add first version of CosmWasmClient
This commit is contained in:
parent
80d0c44616
commit
66fd0800ff
112
packages/sdk/src/cosmwasmclient.spec.ts
Normal file
112
packages/sdk/src/cosmwasmclient.spec.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
147
packages/sdk/src/cosmwasmclient.ts
Normal file
147
packages/sdk/src/cosmwasmclient.ts
Normal file
@ -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<StdSignature>;
|
||||
}
|
||||
|
||||
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<string> {
|
||||
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<number> {
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
22
packages/sdk/types/cosmwasmclient.d.ts
vendored
Normal file
22
packages/sdk/types/cosmwasmclient.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import { Coin, StdSignature } from "./types";
|
||||
export interface SigningCallback {
|
||||
(signBytes: Uint8Array): Promise<StdSignature>;
|
||||
}
|
||||
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<string>;
|
||||
/** Uploads code and returns a code ID */
|
||||
upload(wasmCode: Uint8Array, memo?: string): Promise<number>;
|
||||
instantiate(
|
||||
codeId: number,
|
||||
initMsg: object,
|
||||
memo?: string,
|
||||
transferAmount?: readonly Coin[],
|
||||
): Promise<string>;
|
||||
}
|
||||
1
packages/sdk/types/index.d.ts
vendored
1
packages/sdk/types/index.d.ts
vendored
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user