Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement cw4-group typescript helper #476

Merged
merged 9 commits into from
Oct 15, 2021
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions contracts/cw4-group/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import axios from "axios";
import fs from "fs";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice, calculateFee, StdFee } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet, makeCosmoshubPath } from "@cosmjs/proto-signing";
import { Slip10RawIndex } from "@cosmjs/crypto";
import { toUtf8, toBase64 } from "@cosmjs/encoding";
import path from "path";

/*
* This is a set of helpers meant for use with @cosmjs/cli
* With these you can easily use the cw20 contract without worrying about forming messages and parsing queries.
*
* Usage: npx @cosmjs/cli@^0.26 --init https://raw.githubusercontent.com/CosmWasm/cosmwasm-plus/master/contracts/cw4-group/helpers.ts
orkunkl marked this conversation as resolved.
Show resolved Hide resolved
*
* Create a client:
* const [addr, client] = await useOptions(pebblenetOptions).setup('password');
*
* Get the mnemonic:
* await useOptions(pebblenetOptions).recoverMnemonic(password);
*
* Create contract:
* const contract = CW4Group(client, pebblenetOptions.fees);
*
* Upload contract:
* const codeId = await contract.upload(addr);
*
* Instantiate contract example:
* const initMsg = {
* admin: addr,
* members: [
* {
* addr: "wasm1hkxhcvw6sfyu6ztkce3dlz5nnk8kwjmcd7ettt",
* weight: 10,
* },
* {
* addr: "wasm1z6ms6cejaj8jz8zwkntx9ua0klhtptvz8elaxp",
* weight: 15,
* },
* ]
* };
* const instance = await contract.instantiate(addr, codeId, initMsg, 'WORKFORCE1');
*
* If you want to use this code inside an app, you will need several imports from https://github.com/CosmWasm/cosmjs
*/

interface Options {
readonly httpUrl: string
readonly networkId: string
readonly feeToken: string
readonly bech32prefix: string
readonly hdPath: readonly Slip10RawIndex[]
readonly faucetUrl?: string
readonly defaultKeyFile: string,
readonly fees: {
upload: StdFee,
init: StdFee,
exec: StdFee
}
}

const pebblenetGasPrice = GasPrice.fromString("0.01upebble");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pebble net config is repeated in each helper, right?

Copy link
Contributor Author

@orkunkl orkunkl Oct 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR we need a new ts-helpers repo somewhere for all this code, and it should be published to npm to make life easy for app developers, not just @cosmjs/cli.

Great we had some plans to move to. https://github.com/InterWasm/DAO/issues/26 @findolor

const pebblenetOptions: Options = {
httpUrl: 'https://rpc.pebblenet.cosmwasm.com',
networkId: 'pebblenet-1',
bech32prefix: 'wasm',
feeToken: 'upebble',
faucetUrl: 'https://faucet.pebblenet.cosmwasm.com/credit',
hdPath: makeCosmoshubPath(0),
defaultKeyFile: path.join(process.env.HOME, ".pebblenet.key"),
fees: {
upload: calculateFee(1500000, pebblenetGasPrice),
init: calculateFee(500000, pebblenetGasPrice),
exec: calculateFee(200000, pebblenetGasPrice),
},
}

interface Network {
setup: (password: string, filename?: string) => Promise<[string, SigningCosmWasmClient]>
recoverMnemonic: (password: string, filename?: string) => Promise<string>
}

const useOptions = (options: Options): Network => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as is this "useOptions" code


const loadOrCreateWallet = async (options: Options, filename: string, password: string): Promise<DirectSecp256k1HdWallet> => {
let encrypted: string;
try {
encrypted = fs.readFileSync(filename, 'utf8');
} catch (err) {
// generate if no file exists
const wallet = await DirectSecp256k1HdWallet.generate(12, {hdPaths: [options.hdPath], prefix: options.bech32prefix});
const encrypted = await wallet.serialize(password);
fs.writeFileSync(filename, encrypted, 'utf8');
return wallet;
}
// otherwise, decrypt the file (we cannot put deserialize inside try or it will over-write on a bad password)
const wallet = await DirectSecp256k1HdWallet.deserialize(encrypted, password);
return wallet;
};

const connect = async (
wallet: DirectSecp256k1HdWallet,
options: Options
): Promise<SigningCosmWasmClient> => {
const clientOptions = {
prefix: options.bech32prefix
}
return await SigningCosmWasmClient.connectWithSigner(options.httpUrl, wallet, clientOptions)
};

const hitFaucet = async (
faucetUrl: string,
address: string,
denom: string
): Promise<void> => {
await axios.post(faucetUrl, {denom, address});
}

const setup = async (password: string, filename?: string): Promise<[string, SigningCosmWasmClient]> => {
const keyfile = filename || options.defaultKeyFile;
const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
const client = await connect(wallet, pebblenetOptions);

const [account] = await wallet.getAccounts();
// ensure we have some tokens
if (options.faucetUrl) {
const tokens = await client.getBalance(account.address, options.feeToken)
if (tokens.amount === '0') {
console.log(`Getting ${options.feeToken} from faucet`);
await hitFaucet(options.faucetUrl, account.address, options.feeToken);
}
}

return [account.address, client];
}

const recoverMnemonic = async (password: string, filename?: string): Promise<string> => {
const keyfile = filename || options.defaultKeyFile;
const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
return wallet.mnemonic;
}

return {setup, recoverMnemonic};
}

interface AdminResponse {
readonly admin?: string
}

interface MemberResponse {
readonly weight?: number;
}

interface MemberListResponse {
readonly members: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be something like:

interface MemberListResponse {
  readonly members: []Member;
}

interface Members {
  readonly addr: string;
  readonly weight: number;
}

See:

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Member {
    pub addr: String,
    pub weight: u64,
}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct MemberListResponse {
    pub members: Vec<Member>,
}

}

interface TotalWeightResponse {
readonly weight: number;
}

interface HooksResponse {
readonly hooks: readonly string[];
}

interface CW4GroupInstance {
readonly contractAddress: string

// queries
admin: () => Promise<AdminResponse>
totalWeight: () => Promise<TotalWeightResponse>
member: (addr: string, atHeight?: number) => Promise<MemberResponse>
listMembers: (startAfter?: string, limit?: number) => Promise<MemberListResponse>
hooks: () => Promise<HooksResponse>

// actions
updateAdmin: (txSigner: string, admin?: string) => Promise<string>
updateMembers: (txSigner: string, remove: string[], add: string[] ) => Promise<string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See:

    UpdateMembers {
        remove: Vec<String>,
        add: Vec<Member>,
    },

The add argument is a []Member like above in the query... it must have both the address and the weight

addHook: (txSigner: string, addr: string) => Promise<string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I highly doubt these will be called by any client code. I would remove them to avoid confusing devs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added _ prefix

  // will not used by end user for testing purposes
  _addHook: (txSigner: string, addr: string) => Promise<string>
  _removeHook: (txSigner: string, addr: string) => Promise<string>

removeHook: (txSigner: string, addr: string) => Promise<string>
}

interface CW4GroupContract {
upload: (txSigner: string) => Promise<number>
instantiate: (txSigner: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string) => Promise<CW4GroupInstance>
use: (contractAddress: string) => CW4GroupInstance
}

export const CW4Group = (client: SigningCosmWasmClient, fees: Options['fees']): CW4GroupContract => {
const use = (contractAddress: string): CW4GroupInstance => {

const admin = async (): Promise<AdminResponse> => {
return client.queryContractSmart(contractAddress, {admin: {}});
};

const totalWeight = async (): Promise<TotalWeightResponse> => {
return client.queryContractSmart(contractAddress, {total_weight: {}});
};

const member = async (addr: string, atHeight?: number): Promise<MemberResponse> => {
return client.queryContractSmart(contractAddress, {member: {addr, at_height: atHeight}});
};

const listMembers = async (startAfter?: string, limit?: number): Promise<MemberListResponse> => {
return client.queryContractSmart(contractAddress, {list_members: {start_after: startAfter, limit}});
};

const hooks = async (): Promise<HooksResponse> => {
return client.queryContractSmart(contractAddress, {hooks: {}});
};

const updateAdmin = async (txSigner: string, admin?: string): Promise<string> => {
const result = await client.execute(txSigner, contractAddress, {update_admin: {admin}}, fees.exec);
return result.transactionHash;
}

const updateMembers = async (txSigner: string, remove: string[], add: string[]): Promise<string> => {
const result = await client.execute(txSigner, contractAddress, {update_members: {remove, add}}, fees.exec);
return result.transactionHash;
}

const addHook = async (txSigner: string, addr: string): Promise<string> => {
const result = await client.execute(txSigner, contractAddress, {add_hook: {addr}}, fees.exec);
return result.transactionHash;
}

const removeHook = async (txSigner: string, addr: string): Promise<string> => {
const result = await client.execute(txSigner, contractAddress, {remove_hook: {addr}}, fees.exec);
return result.transactionHash;
}

return {
contractAddress,
admin,
totalWeight,
member,
listMembers,
hooks,
updateAdmin,
updateMembers,
addHook,
removeHook
};
}

const downloadWasm = async (url: string): Promise<Uint8Array> => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper may belong in a standard library or something.

const r = await axios.get(url, { responseType: 'arraybuffer' })
if (r.status !== 200) {
throw new Error(`Download error: ${r.status}`)
}
return r.data
}

const upload = async (senderAddress: string): Promise<number> => {
const sourceUrl = "https://github.com/CosmWasm/cosmwasm-plus/releases/download/v0.9.0/cw4_group.wasm";
const wasm = await downloadWasm(sourceUrl);
const result = await client.upload(senderAddress, wasm, fees.upload);
return result.codeId;
}

const instantiate = async (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<CW4GroupInstance> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, fees.init, { memo: `Init ${label}`, admin });
return use(result.contractAddress);
}

return { upload, instantiate, use };
}