Skip to content

Instantly share code, notes, and snippets.

@dakom
Last active May 22, 2024 09:39
Show Gist options
  • Save dakom/d75b3d94f17166a3be4d1ff2c734141c to your computer and use it in GitHub Desktop.
Save dakom/d75b3d94f17166a3be4d1ff2c734141c to your computer and use it in GitHub Desktop.
Cosmwasm notes

Table of contents

  1. Running wasmd
  2. Gas and limits
  3. IBC
    1. Relaying
    2. Contracts
  4. Multitest
  5. Submessages
  6. Endpoints
  7. Raw data
  8. Database dumps
  9. Determinstic types
  10. Hacking wasmd
  11. Custom modules in clients

Running Wasmd

Explicit local build

after make install (or on apple silicon: LEDGER_ENABLED=false make install)

our chain-id is going to be localwasmd our gas denomination is going to be configured to uwasm staking denomination is the default ustake

the following sortof mimics the explicit steps in https://github.com/CosmWasm/wasmd/blob/main/contrib/local/setup_wasmd.sh

adding two users: tester1 and validator1

  • wipe ~/.wasmd if it exists
  • wasmd init localwasmd --chain-id localwasmd --overwrite
  • edit ~/.wasmd/config/app.toml
    • set minimum-gas-prices to 0.025uwasm
  • edit ~/.wasmd/config/client.toml
    • set chain-id to localwasmd
  • edit ~/.wasmd/config/genesis.json
    • change stake to ustake
  • edit ~/.wasmd/config/config.toml
    • under Consensus Configuration: change all the timeout_ stuff to 200ms for faster local dev block time
  • wasmd keys add tester1
  • wasmd add-genesis-account $(wasmd keys show -a tester1) 10000000000uwasm,10000000000ustake
  • wasmd keys add validator1
  • wasmd add-genesis-account $(wasmd keys show -a validator1) 10000000000uwasm,10000000000ustake
  • wasmd gentx validator1 "250000000ustake" --chain-id="localwasmd" --amount="250000000ustake"
  • wasmd collect-gentxs

now wasmd start should just work

for hacking around the sourcecode, see Hacking wasmd

Docker

Just follow the Readme

But for convenience, in Taskfile syntax where you would then just run task dev:

version: '3'

vars:
  ADDRS: 
    address-1 
    address-2
  PASSWORD: some-password1234567!

tasks:
  start:
    env: 
      DOCKER_DEFAULT_PLATFORM: linux/amd64

    cmds:
      - docker volume rm -f wasmd_data
      - docker run --rm -it
        -e PASSWORD={{.PASSWORD}}
        --mount type=volume,source=wasmd_data,target=/root
        cosmwasm/wasmd:latest 
        /opt/setup_wasmd.sh {{.ADDRS}}
      - docker run --rm -it -p 26657:26657 -p 26656:26656 -p 1317:1317
        --mount type=volume,source=wasmd_data,target=/root
        cosmwasm/wasmd:latest 
        /opt/run_wasmd.sh

Gas and limits

Keys can be "complex" and are concatenated with an internal namespacing scheme, which both contribute to their hard limits.

Max gas for executions is per-block and configured in the genesis file, e.g.

  "consensus_params": {
    "block": {
      "max_gas": "SET_YOUR_MAX_VALUE",  

Max gas for queries is configured per-node the app.toml

There are also size limits, such as the maximum transaction size, defined in the config.toml max_tx_bytes var. This applies to all transactions, including code upload.

VM computational costs are tuned to be very cheap. Not free, but, for typical financial products, it's unlikely to be the culprit for runaway gas costs

Smart queries are generally cheap themselves, but incur the same contract loading cost as the initial query (60k or so). Therefore they are, in practice, quite expensive. This can be avoided by voting to "pin" hot contracts, but generally speaking, use raw queries where possible.

Optimization

Generally speaking, costs from most expensive to most cheap:

  1. Contract Load (happens when you smart-query a contract that isn't pinned) - roughly 60,000 SDK units
  2. Storage (see above - both the frequency of read/write actions and the size of data matter). Can easily add up to thousands of SDK units for simple usage
  3. Computation - relatively cheap, tuned to be about 1000 VM instructions per 1 sdk unit.

In practical terms, if you're optimizing for gas, consider the following:

  1. More monolothic contracts to avoid cross-contract queries
  2. Raw queries instead of smart queries
  3. Think very carefully about storage access patterns
  4. Cache storage lookups that stay consistent through a call in memory
  5. Don't worry about simple typical code like iterating over a Vec or cloning a String. It' relatively negligible. Heavy computation like calculating a hash can take around 100 gas units (still not worrisome, in most cases)

IBC

The following uses juno/osmosis testnets and the Go relayer as examples, but the concepts are tool-agnostic

Relaying: One-time setup

1. Add the chain config for each chain

rly chains add --file ./juno-testnet.json juno-testnet
rly chains add --file ./osmosis-testnet.json osmosis-testnet

Here's an example config file for Juno testnet:

{
    "type": "cosmos",
    "value": {
      "key": "default",
      "chain-id": "uni-6",
      "rpc-addr": "https://rpc.uni.junonetwork.io:443",
      "account-prefix": "juno",
      "keyring-backend": "test",
      "gas-adjustment": 1.5,
      "gas-prices": "0.0026ujunox",
      "debug": true,
      "timeout": "20s",
      "output-format": "json",
      "sign-mode": "direct"
    }
}

2. Add a wallet for each chain

rly keys restore juno-testnet default "[MNEMONIC]"
rly keys restore osmosis-testnet default "[MNEMONIC]"

Sanity check that network config is right and wallets have gas tokens

rly q balance juno-testnet
rly q balance osmosis-testnet

3. Create a "path"

This is a go-relayer config thing, not general IBC. We decide the path name, which is “foo” here. Note that here it uses the ChainId, not the network names from above. foo (or whatever path name you choose) will be used in more commands later, instead of referencing each chain every time.

rly paths new uni-6 osmo-test-5 foo

4. Create "clients"

I believe it needs to be made from each side (using the path name from above)

rly transact client juno-testnet osmosis-testnet foo --debug --override
rly transact client osmosis-testnet juno-testnet foo --debug --override

5. Create a "connection"

rly transact connection foo --debug --override

Relaying: creating a "port"

This is assigned automatically when you upload a contract with the required IBC handlers, and the port can be queried by looking up the contract info, e.g. with a cosmjs client.getContract() call.

Since the contract/port has changed, you'll need to create the channels from scratch too.

Relaying: creating a channel

1. Decide the channel version

This is just some string that the protocol should be aware of for sanity checking and compatibility. Here it will be "bar-001". This version can stay consistent as you create channels, it's just a way to make sure both sides of the channel understand eachother.

A contract may have multiple channels, e.g. for connecting a contract on one chain to contracts on multiple other chains (e.g. "mint nfts" on stargaze, "send tokens" on osmosis, etc.)

2. Create a "channel"

rly transact channel foo --src-port [wasm.somejunoaddress] --dst-port [wasm.someosmosisaddress] --order unordered --version bar-001 --debug --override

That's it!

Now we can start the relayer:

rly start foo --debug

IBC for Contracts

There are more steps involved here, but TL;DR:

Entrypoints

// called on both sides, `OpenInit` and `OpenTry`, but logic is usually the same
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(deps: DepsMut, env: Env, msg: IbcChannelOpenMsg) -> Result<IbcChannelOpenResponse> {
    // validate channel version, ordering, etc.
    // almost always want to return `None` here
    Ok(None)
}

// called on both sides when channel is connected
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_connect(deps: DepsMut, env: Env, msg: IbcChannelConnectMsg) -> Result<IbcBasicResponse> {
  // also validate channel like in ibc_channel_open
  // it's possible for a contract to connect to multiple channels - and this can be differentiated by checking `msg.channel().version`
  // store channel in state

  Ok(IbcBasicResponse::default())
}

// called on destination side when a packet is received
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(deps: DepsMut, env: Env, msg: IbcPacketReceiveMsg) -> Result<IbcReceiveResponse> {
  // parse `msg.packet.data`
  // see next section about acknowledgements and error handling
}

// called on sender side when the destination side acknowledges the packet
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_ack(deps: DepsMut, env: Env, ack: IbcPacketAckMsg) -> Result<IbcBasicResponse> {
  // if anything needs to be finalized on the sender side:
  // check ack.acknolwedgement.data
  // parse ack.original_packet.data
}

Acknowledgements and Error Handling

When the receiving side is finished processing an IBC packet, it may set the acknowledgement in the IbcReceiveResponse type. This data does not have any inherent meaning, but it is likely to require keeping it consistent so that the sender can check the acknowledgement for success, error, etc.

If a contract errors out (e.g. returns Err instead of Ok), then the CosmWasm module on the go side will automatically revert the contract state and turn the acknowledgement into a predefined error acknowledgement type corresponding to StdAck. In this case, the error can be recovered via:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_ack(deps: DepsMut, env: Env, ack: IbcPacketAckMsg) -> Result<IbcBasicResponse> {
  let data:StdAck = from_json(ack.acknowledgement.data)?;
  if let StdAck::Error(err) = data {
    // do something with error
  }
  //...
}

Sending a packet

// write an IBC packet to my channel
// this channel info was stored in `ibc_channel_connect()`
let channel_id = self
    .get_ibc_channel(storage)?
    .endpoint
    .channel_id;

response.add_message(IbcMsg::SendPacket {
    channel_id,
    data: to_binary(&msg)?,
    timeout: IbcTimeout::with_timestamp(self.env.block.time.plus_seconds(TIMEOUT_SECONDS)),
});

Some references

Multitest

Defining a contract

The overall idea is that it must be something that satisfies the Contract trait in multitest.

Out of the box, this can be done with ContractWrapper, as per the docs: https://docs.rs/cw-multi-test/latest/cw_multi_test/#contracts

Running a contract

Once you have a valid contract, it's very simple:

let mut app = App::default();
let code_id = app.store_code(get_contract());
let contract_addr = app.instantiate_contract(
    code_id,
    Addr::unchecked("sender-addr"),
    &InstantiateMsg { },
    &[],
    "mycontract",
    Some("admin-addr"),
)?;

// query
app.wrap().query_wasm_smart(contract_addr, &QueryMsg::Foo {});

// execute
let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute {
    contract_addr: contract_addr.to_string(),
    msg: to_binary(ExecuteMsg::Foo{})?,
    funds: vec![]
});
app.execute(sender.clone(), cosmos_msg)?;

// update block time
app.update_block(|block_info| {
  block_info.time = block_info.time.plus_nanos(/* some nanos, to emulate chain speed */);
  block_info.height += 1;
})

Minting tokens

app.sudo(SudoMsg::Bank(BankSudo::Mint {
    to_address: recipient.to_string(),
    amount: vec![Coin::new(100, "uwasm")]
}));

let balance = app.wrap().query_balance(recipient, "uwasm")?;

Submessages

The full semantics for submessages are described in the doc

One important takeaway is that if you have a reply handler for submessage error (ReplyOn::Always or ReplyOn::Error), then only the sub-contract state is reverted.

For example, a contract may send a submessage to execute itself, and gain the following ability:

  • if the submessage errors, all the state from that call in the subcontract is reverted
  • if the reply handler returns Ok, then it can still change state in this parent level (if it returns Err then its state is reverted too, of course)

This is a useful pattern for creating, effectively, sub-transactions and not having to manually roll-back state while still mutating some state like a log of failures.

Endpoints

See the registry at https://github.com/cosmos/chain-registry/tree/master (testnets are a subdir: https://github.com/cosmos/chain-registry/tree/master/testnets)

Each chain has a JSON config that specifies some RPC/GRPC endpoints.

Websockets might exist on the rpc node with wss:// and /websocket at the end, but YMMV

Chains generally also provide an LCD endpoint for doing things like querying a native token price at a specific block height adding ?height=BLOCKNUMBER.

Raw data

Decoding raw data

Use base64 and protobuf to decode raw data. Example:

echo "CrgBCrABCiYvdGVycmEud2FzbS52MWJldGExLk1zZ0V4ZWN1dGVDb250cmFjdBKFAQosdGVycmExcDAyNDJnZXdweDhwY3I4aG5wNTd5bG16ZWY2bWtwNjY0eXN4dzQSLHRlcnJhMWc3ampqa3Q1dXZramV5aHA4ZWNkejRlNGh2dG44M3N1ZDN0bWgyGid7ImV4ZWN1dGVfc3RyYXRlZ3kiOnsic3RyYXRlZ3lfaWQiOjI2fX0Ym6rpAhJqClEKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiEDAgrw7ozay5jSmwGydzr3hZ57XKLZhheZ+C2AIAIjBLoSBAoCCH8YuHUSFQoOCgR1dXNkEgY1NjM1NzMQ3KjlARpAqjHfsg1dT8Lg9SeqgOC50m55Hmu1dY4heKdbV09oGp0Jg1LLLcsNbhcgFjicZ23frMdLpysjIF4Xe65qiQZ35Q==" | base64 -D | protoc --decode_raw

Raw querying storage

Here's a bit of helper code to load cw-storage-plus things via raw queries. This is adapted from https://github.com/CosmWasm/cw-storage-plus/blob/69300779519d8ba956fb53725e44e2b59c317b1c/src/helpers.rs#L57 originally, and the abstraction is copy/pasted with a minor tweak from the Levana sourcecode

/// Load an [cw_storage_plus::Item] stored in an external contract
pub fn load_external_item<T: serde::de::DeserializeOwned>(
    querier: &QuerierWrapper<Empty>,
    contract_addr: impl Into<String>,
    key: impl Into<Binary>,
) -> anyhow::Result<T> {
    // only deserialize for extra context if in debug mode
    // because we must pass the key in as an owned value
    // and so we have to extract the name in the happy path too
    let key: Binary = key.into();
    let debug_key_name = if cfg!(debug_assertions) {
        from_binary::<String>(&key).ok()
    } else {
        None
    };

    external_helper(querier, contract_addr, key, || {
        anyhow!("unable to load external item {}", debug_key_name.unwrap_or_default())
    })
}

/// Load a value from a [cw_storage_plus::Map] stored in an external contract
pub fn load_external_map<'a, T: serde::de::DeserializeOwned>(
    querier: &QuerierWrapper<Empty>,
    contract_addr: impl Into<String>,
    namespace: &str,
    key: &impl PrimaryKey<'a>,
) -> anyhow::Result<T> {
    external_helper(querier, contract_addr, map_key(namespace, key), || {
        anyhow!("unable to load external map {}", namespace)
    })
}

/// Check if a [cw_storage_plus::Map] in an external contract has a specific key
pub fn external_map_has<'a>(
    querier: &QuerierWrapper<Empty>,
    contract_addr: impl Into<String>,
    namespace: &str,
    key: &impl PrimaryKey<'a>,
) -> anyhow::Result<bool> {
    querier
        .query_wasm_raw(contract_addr, map_key(namespace, key))
        .map(|x| x.is_some())
        .map_err(|e| e.into())
}

fn external_helper<T: serde::de::DeserializeOwned>(
    querier: &QuerierWrapper<Empty>,
    contract_addr: impl Into<String>,
    key: impl Into<Binary>,
    mk_error_message: impl FnOnce() -> anyhow::Error,
) -> anyhow::Result<T> {
    let res = querier
        .query_wasm_raw(contract_addr, key)?
        .ok_or_else(mk_error_message)?;
    serde_json_wasm::from_slice(&res).map_err(|err| err.into())
}

pub fn map_key<'a, K: PrimaryKey<'a>>(namespace: &str, key: &K) -> Vec<u8> {

    let mut size = namespace.len();
    let key = key.key();
    assert!(!key.is_empty());

    for x in &key {
        size += x.as_ref().len() + 2;
    }

    let mut out = Vec::<u8>::with_capacity(size);

    for prefix in std::iter::once(namespace.as_bytes())
        .chain(key.iter().take(key.len() - 1).map(|key| key.as_ref()))
    {
        out.extend_from_slice(&encode_length(prefix));
        out.extend_from_slice(prefix);
    }

    if let Some(last) = key.last() {
        out.extend_from_slice(last.as_ref());
    }

    out
}

fn encode_length(bytes: &[u8]) -> [u8; 2] {
    if let Ok(len) = u16::try_from(bytes.len()) {
        len.to_be_bytes()
    } else {
        panic!("only supports namespaces up to length 0xFFFF")
    }
}

Database dumps

Available via some third-party providers, e.g. https://quicksync.io/networks/osmosis.html

From app.toml:

# default: the last 100 states are kept in addition to every 500th state; pruning at 10 block intervals
# nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node)
# everything: all saved states will be deleted, storing only the current state; pruning at 10 block intervals
# custom: allow pruning options to be manually specified through 'pruning-keep-recent', 'pruning-keep-every', and 'pruning-interval'
pruning = "default"

Deterministic types

Contract types must be deterministic, i.e. re-runing the same code with the same state and inputs must always produce the same results. Therefore, by default, these types cannot be used as they depend on randomization:

  • HashMap
  • HashSet

Additionally, usize is not allowed since it would be different when running off-chain tests (which typically run on 64 bit architectures), and there have historically been some problems with serialization.

Instead, use BTreeMap, which is deterministic, and u32 explicitly.

Hacking WASMD

Running WASMD to see logs

  1. wasmd start --log_level panic --trace (panic may be a bit extreme - point is some level lower than info, which is just way too noisy to see anything)
  2. deps.api.debug() contract messages will now appear in the wasmd console

Making changes

Some tips for how it all fits together: CosmWasm/cosmwasm#746 (comment)

SETUP

  1. in wasmd go.mod replace( github.com/CosmWasm/wasmvm => path/to/my/wasmvm)
  2. in local wasmvm edit libwasmvm's Cargo.toml to point at a local cosmwasm-vm via path = ...
  3. generally point at local crates as-needed (e.g. a local cosmwasm-std, local cw-storage-plus, etc.)

ITERATIVE DEV

  1. Make changes in local cosmwasm-vm
  2. recompile the libwasmvm (make build-rust in wasmvm)
  3. rebuild wasmd so it will use this new wasmvm build (LEDGER_ENABLED=false make install on osx)

e.g. editing do_debug() like this will show some interesting info:

println!("GAS LEFT: {}", env.get_gas_left());
env.with_gas_state(|gas_state| {
    println!("GAS STATE: {:?}", gas_state);
});

Custom modules in clients

Out of the box, cosmjs comes with support for popular modules like Bank, CosmWasm, etc.

However, a chain usually builds on wasmd in order to add new modules, which in turn requires that clients be aware of this. Usually, the chains then provide a custom client implementation, however, this isn't strictly necessary since CosmJS provides a way to create methods using custom protobuf definitions.

The official documentation is here: https://github.com/cosmos/cosmjs/blob/main/packages/stargate/CUSTOM_PROTOBUF_CODECS.md

First we need to build the definitions, then we can use them in client code.

Build protobuf definitions

The steps are:

  1. download all the protobuf files necessary
    • this requires some digging as there are often interdependencies
  2. compile all the protobuf definitions with the typescript generation plugin

here's an example in Taskfile where you'd run task generate to generate Osmosis tokenfactory protobuf definitions from start to finish.

This assumes you have protoc installed and a scripts directory with just the ts-proto node_modules dependency (i.e. a minimal npm/yarn package.json)

version: '3'

vars:
  PROTO_TEMP_DIR: proto-temp
  PROTO_OUTPUT_DIR: proto-ts
  COSMOS_SDK_VERSION: 'v0.47.1'
  COSMOS_PROTO_VERSION: 'v1.0.0-beta.3'
  OSMOSIS_VERSION: 'master'
  REGEN_VERSION: 'v1.3.3-alpha.regen.1'
  GOOGLE_VERSION: 'master'
  COSMOS_SDK_BASE_PATH: "cosmos/base/v1beta1"
  COSMOS_SDK_QUERY_PATH: "cosmos/base/query/v1beta1"
  COSMOS_SDK_BANK_PATH: "cosmos/bank/v1beta1"
  COSMOS_SDK_AMINO_PATH: "amino"
  COSMOS_SDK_MSG_PATH: "cosmos/msg/v1"

tasks:
  generate:
    cmds:
      - task: download 
      - task: compile 
      - echo "Finished"

  download:
    deps: [
        "download-proto-cosmos",
        "download-proto-cosmos-sdk",
        "download-proto-gogo",
        "download-proto-google",
        "download-proto-tokenfactory"
    ]

  compile:
    cmds:
      - mkdir -p "{{.PROTO_OUTPUT_DIR}}"
      - cd scripts && protoc --plugin="./node_modules/.bin/protoc-gen-ts_proto" --ts_proto_out="../{{.PROTO_OUTPUT_DIR}}" --proto_path="../{{.PROTO_TEMP_DIR}}" --ts_proto_opt="esModuleInterop=true,forceLong=long,useOptionals=all" "../{{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/tx.proto" "../{{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/query.proto"

  download-proto-cosmos:
    cmds:
      - mkdir -p "{{.PROTO_TEMP_DIR}}/cosmos_proto"
      - curl https://raw.githubusercontent.com/cosmos/cosmos-proto/{{.COSMOS_PROTO_VERSION}}/proto/cosmos_proto/cosmos.proto -o {{.PROTO_TEMP_DIR}}/cosmos_proto/cosmos.proto


  download-proto-cosmos-sdk:
    cmds:
      - mkdir -p "{{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_BASE_PATH}}"
      - mkdir -p "{{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_QUERY_PATH}}"
      - mkdir -p "{{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_BANK_PATH}}"
      - mkdir -p "{{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_AMINO_PATH}}"
      - mkdir -p "{{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_MSG_PATH}}"
      - curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/{{.COSMOS_SDK_VERSION}}/proto/{{.COSMOS_SDK_BASE_PATH}}/coin.proto -o {{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_BASE_PATH}}/coin.proto
      - curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/{{.COSMOS_SDK_VERSION}}/proto/{{.COSMOS_SDK_QUERY_PATH}}/pagination.proto -o {{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_QUERY_PATH}}/pagination.proto
      - curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/{{.COSMOS_SDK_VERSION}}/proto/{{.COSMOS_SDK_BANK_PATH}}/bank.proto -o {{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_BANK_PATH}}/bank.proto
      - curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/{{.COSMOS_SDK_VERSION}}/proto/{{.COSMOS_SDK_AMINO_PATH}}/amino.proto -o {{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_AMINO_PATH}}/amino.proto
      - curl https://raw.githubusercontent.com/cosmos/cosmos-sdk/{{.COSMOS_SDK_VERSION}}/proto/{{.COSMOS_SDK_MSG_PATH}}/msg.proto -o {{.PROTO_TEMP_DIR}}/{{.COSMOS_SDK_MSG_PATH}}/msg.proto

  # actually download from regen, see https://github.com/cosmos/cosmos-sdk/issues/12984#issuecomment-1275674526
  download-proto-gogo:
    cmds:
      - mkdir -p {{.PROTO_TEMP_DIR}}/gogoproto
      - curl https://raw.githubusercontent.com/regen-network/protobuf/{{.REGEN_VERSION}}/gogoproto/gogo.proto -o {{.PROTO_TEMP_DIR}}/gogoproto/gogo.proto 

  download-proto-google:
    cmds: 
      - mkdir -p {{.PROTO_TEMP_DIR}}/google/api
      - curl https://raw.githubusercontent.com/googleapis/googleapis/{{.GOOGLE_VERSION}}/google/api/annotations.proto -o {{.PROTO_TEMP_DIR}}/google/api/annotations.proto
      - curl https://raw.githubusercontent.com/googleapis/googleapis/{{.GOOGLE_VERSION}}/google/api/http.proto -o {{.PROTO_TEMP_DIR}}/google/api/http.proto

  download-proto-tokenfactory:
    cmds:
      - mkdir -p {{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1
      - curl https://raw.githubusercontent.com/osmosis-labs/osmosis/{{.OSMOSIS_VERSION}}/proto/osmosis/tokenfactory/v1beta1/authorityMetadata.proto -o {{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/authorityMetadata.proto
      - curl https://raw.githubusercontent.com/osmosis-labs/osmosis/{{.OSMOSIS_VERSION}}/proto/osmosis/tokenfactory/v1beta1/genesis.proto -o {{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/genesis.proto
      - curl https://raw.githubusercontent.com/osmosis-labs/osmosis/{{.OSMOSIS_VERSION}}/proto/osmosis/tokenfactory/v1beta1/params.proto -o {{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/params.proto
      - curl https://raw.githubusercontent.com/osmosis-labs/osmosis/{{.OSMOSIS_VERSION}}/proto/osmosis/tokenfactory/v1beta1/query.proto -o {{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/query.proto
      - curl https://raw.githubusercontent.com/osmosis-labs/osmosis/{{.OSMOSIS_VERSION}}/proto/osmosis/tokenfactory/v1beta1/tx.proto -o {{.PROTO_TEMP_DIR}}/osmosis/tokenfactory/v1beta1/tx.proto

Use protobuf definitions

Assuming we have a project setup with all the dependencies and the output from the above step in src/proto-ts for a project, we can then do something like this to add functionality to our client instance:

import { Registry } from "@cosmjs/proto-signing";
import { Event, QueryClient, createProtobufRpcClient } from "@cosmjs/stargate";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { QueryClientImpl } from "./proto-ts/osmosis/tokenfactory/v1beta1/query";
import {protobufPackage, MsgCreateDenom, MsgChangeAdmin, MsgSetBeforeSendHook, MsgMint,} from "./proto-ts/osmosis/tokenfactory/v1beta1/tx";
import {Coin} from "./proto-ts/cosmos/base/v1beta1/coin";

const TypeUrl = {
    CreateDenom: `/${protobufPackage}.MsgCreateDenom`,
    ChangeAdmin: `/${protobufPackage}.MsgChangeAdmin`,
    SetBeforeSendHook: `/${protobufPackage}.MsgSetBeforeSendHook`,
    Mint: `/${protobufPackage}.MsgMint`,
};

(async () => {

    // Create the client
    const signer = await DirectSecp256k1HdWallet.fromMnemonic(
        "my super secret seed phrase", 
        { 
            prefix: "whatever-addr-prefix",
        }
    );

    const accounts = await signer.getAccounts()
    const walletAddress = accounts[0].address

    const client = await SigningCosmWasmClient.connectWithSigner(
        "http://rpc.example.com",
        signer,
        { 
          gasPrice: GasPrice.fromString("0.0125uwasm") 

        }
    );

    // Register the new protobuf extensions
    client.registry.register(TypeUrl.CreateDenom, MsgCreateDenom);
    client.registry.register(TypeUrl.ChangeAdmin, MsgChangeAdmin);
    client.registry.register(TypeUrl.SetBeforeSendHook, MsgSetBeforeSendHook);
    client.registry.register(TypeUrl.Mint, MsgMint);

    // Use it!
    const resp = await client.signAndBroadcast(
        wallet.address,
        [
            {
                typeUrl: TypeUrl.CreateDenom, 
                value: MsgCreateDenom.fromPartial({
                    sender: walletAddress,
                    subdenom: "mynewdenom"
                })
            }
        ],
        "auto",
    );

    console.log(`Transaction success: ${resp.transactionHash}`);
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment