Skip to content

Proof of concept for Rust types » TypeScript #8

@mikedotexe

Description

@mikedotexe

Please take a look at this repo:
https://github.com/InterWasm/cw-contracts

They demonstrate various contracts and some of them are on the simple side. Let's take cw-to-do-list for example.
The smart contract is in Rust and the source is in the src folder, usually starting with lib.rs because technically we're compiling Rust as a library. You do this as well when you wanna write Rust that compiles to WebAssembly and use it in the browser.

This is a pretty neat demo to check out. Stop work and have fun with this for a bit.
https://wasmbyexample.dev/examples/hello-world/hello-world.rust.en-us.html#project-setup


Neat, huh?

Okay let's explain what's happening, but in a way that's going to skip a lot.

You deploy this smart contract by "uploading it" essentially, to a place where you and other people can instantiate versions of it. After a successful upload, the protocol returns you a "code id" which is a number. You'll use that number to instantiate the smart contract with parameters. For this to-do list example, there's only one parameter, owner. When you or other people instantiate this contract, you do so with a special Instantiate Message. This is different than other blockchains where you might simply call a function named init or something, rolling your own logic to make sure it can't get called more than once.

In Cosmos, this special Instantiate Message is lobbed into the protocol and returns to you a smart contract address, where the protocol has deployed your new contract. This is your instance you'll point to when interacting with a dApp eventually. It's useful to note that this contract address stores its own state, like the owner and other key-value pair(s) you're keeping track of, but the contract code still lives in the cubby hole you uploaded it, too.

It's rather efficient and you can see that Ethereum can do something similar. In NEAR there was no such thing, and if multiple people want to have their own version of a smart contract, they'll need to duplicate the bytecode. Moving on…

Let's trace what happens when you instantiate a contract.

You might use JavaScript (with CosmJS libraries) but since tools aren't there yet (and we'll be building them) you'll likely use a tool like wasmd. This is a pretty helpful tutorial, even though it has bugs:
https://tutorials.cosmos.network/academy/3-my-own-chain/cosmwasm.html#instantiate-your-smart-contract

A little ways down you'll see a command:

wasmd tx wasm instantiate …

That's where we'll start, and follow what happens next.

As we established, the contract compilation starts in lib.rs but in the CosmWasm code patterns that are emerging, that file is pretty small and just loads other files, meant to separate concerns. We'll want to look in the contract.rs file, which is typically where entry points are defined. (Entry points meaning, "here's a place where you can call into a public function on the smart contract from the outside.")

You can see we have this attribute macro above the instantiate function, which is a reserved function name:

https://github.com/InterWasm/cw-contracts/blob/c0a406090d693813c598058d13b21addd7583690/contracts/cw-to-do-list/src/contract.rs#L18-L24

(Damn you, GitHub) here's the code so you don't have to navigate away:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: InstantiateMsg, // ⚛️ lookie here ⚛️
) -> Result<Response, ContractError> {

Note the InstantiateMsg type we're expecting to receive here. We typically put these types in another file to keep things organized: msg.rs.

https://github.com/InterWasm/cw-contracts/blob/c0a406090d693813c598058d13b21addd7583690/contracts/cw-to-do-list/src/msg.rs#L5-L8

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub owner: Option<String>,
}

So we only have 1 parameter that's optional. (If you don't provide it, it'll default to the sender.) You'll use Option a lot, which I recently learned is called a monad after watching the third CosmWasm By Dummies episode, which is a fun watch.

Let's skip the code inside the instantiate function, and move to the next thing you'll likely do, executing a function on the smart contract that'll store something.

We use an Execute Message for this, and tracing this is a little different.

We want to add a new entry to our to-do list. To do this we'll send an ExecuteMsg payload to the entry point execute, which is also a reserved function name just like instantiate.

But unlike instantiation, we'll typically see something different here.

https://github.com/InterWasm/cw-contracts/blob/c0a406090d693813c598058d13b21addd7583690/contracts/cw-to-do-list/src/contract.rs#L44-L51

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg, // ⚛️ this is not a struct, but an enum ⚛️
) -> Result<Response, ContractError> {
    match msg {}
}

Typically (but you can do whatever) you'll have one instantiate payload, but multiple execute payloads that might happen, so we use an Enum and match it, which may remind you of a switch in other languages.

Basics here: https://doc.rust-lang.org/book/ch06-02-match.html

Now this next part I kinda dislike, since it isn't ergonomic to my IDE, but you'll see that inside the match we're essentially "routing" the enum variant we've received and pointing it to an internal function that actually does the work.

Let's look at the first variant we'd like to use:

match msg {
    ExecuteMsg::NewEntry {
        description,
        priority,
    } => execute_create_new_entry(deps, info, description, priority),

and we bop over to the msg.rs file again and peek inside the variant:

pub enum ExecuteMsg {
    NewEntry {
        description: String,
        priority: Option<Priority>,
    },

This part of the official Rust book (super good) talks about Enum variants and how you can:

put any kind of data inside an enum variant: strings, numeric types, or structs, for example. You can even include another enum!

~ https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html#enum-values

which is exactly what this NewEntry variant is doing, as it has description and priority fields.

If you scroll up a bit, you'll see we're passing those to the execute_create_new_entry function, along with other stuff that's available to all Execute Messages, so we can know about the sender, our current blockchain environment, etc.

So broadly, when we send an Execute Message, we're going to load a little piece of state (where we're storing our to-do items), update/insert/delete a to-do item, and then return a result of how that went. Notice the result usually looks like this:

    Ok(Response::new()
        .add_attribute("method", "execute_create_new_entry")
        .add_attribute("new_entry_id", id.to_string()))

These add_attribute things are kinda interesting. A good bookmark explains this well:

attributes: A list of key-value pairs to define emitted SDK events that can be subscribed to and parsed by clients.

~ https://docs.cosmwasm.com/dev-academy/develop-smart-contract/intro/#execution-logic

In this function there are two ways they're dealing with errors:

Returning a specified error when the sender/caller is not the owner:

if info.sender != owner {
    return Err(ContractError::Unauthorized {});
}

(This lives in the error.rs file, and I'd argue they should have returned something with structure instead of a string, oh well.)

The second way they're dealing with an error is with the question marks ? like here:

let owner = CONFIG.load(deps.storage)?.owner;

If somehow, the contract failed to load the key for CONFIG (which would mean the smart contract author did a whoopsies) then it'll blow up with a useful error automatically.

Let's actually talk about the CONFIG for a second. Our smart contract has cubby holes where we can store stuff, designating the name of the key for the key-value pair. By emerging convention, these are typically stored in the state.rs file, like here:

pub const CONFIG: Item<Config> = Item::new("config");

I might become that "well, actually" guy and say that we should just save this key as "c" instead of "config" just to save a teensy bit of ones and zeroes; but whatever.

In that same file you'll notice there are two types of things being stored:

  1. Item
  2. Map

These come from the cw-storage-plus Rust crate here (very good README worth your time):
https://crates.io/crates/cw-storage-plus

and we should basically always use this for now. Maybe we'd deviate on special, advanced cases at some point.

Note that a big drawback I've found is that you can't store a Map in a Map, like you can on NEAR. :/

For now, I think we'll follow Callum's approach of using a composite key if we're tempted to have a map in a map:

One user can vote on many polls.
So how can we store a user's vote across multiple polls?
We're going to use a composite key … in the format of (Addr, String).

~ https://github.com/Callum-A/cosmwasm-zero-to-hero/tree/83333b1c9388c583c6de9d18b88d86246aa693b4/05%20-%20State#part-five---state

pub const BALLOTS: Map<(Addr, String), Ballot> = Map::new("ballots");
// or just "b" instead of "ballots" 😇

Anyway, that's the basic tour, and I'd recommend following this guide linked earlier to get the hands-on vibe:
https://tutorials.cosmos.network/academy/3-my-own-chain/cosmwasm.html

(I ran into errors but submitted PRs and a few got merged, but it's possible a couple commands don't work and you have to fiddle.)


Anyway², I started writing this issue regarding TypeScript and haven't mentioned a damn thing on that yet. This has turned into a decent onboarding guide, but let's bring it home.

The final Message type we'll talk about is the Query Message. Briefly, let's see how it follows the same pattern:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::QueryEntry { id } => to_binary(&query_entry(deps, id)?),}
}

which, for that variant will call query_entry:

fn query_entry(deps: Deps, id: u64) -> StdResult<EntryResponse> {
    let entry = LIST.load(deps.storage, id)?;
    Ok(EntryResponse {
        id: entry.id,
        description: entry.description,
        status: entry.status,
        priority: entry.priority,
    })
}

which uses an EntryResponse that we've got in our msg.rs:

pub enum QueryMsg {
    QueryEntry {
        id: u64,
    },}

and actually this is a bad example because, even though you'll probably never store that many to-do list items, you shouldn't get in the habit of returning a primitive u64 in a response, because JSON has limitations when you try to pass stuff that's bigger than 2^53.

Cuz:

numbers which are integers and are in the range [-(253)+1, (253)-1] are interoperable in the sense that implementations will agree exactly on their numeric values.

~ https://tools.ietf.org/id/draft-ietf-json-rfc4627bis-09.html#rfc.section.6

So once you get higher than that, it's no longer interoperable or reliable. So to get around that, blockchains like Cosmos should stringify the numbers that have potential to get that big.

Back on track, tho. While testing the smart contract either on a local blockchain or on testnet, you might use a terminal command like:

wasmd query wasm contract-state smart …

~ https://tutorials.cosmos.network/academy/3-my-own-chain/cosmwasm.html#call-your-smart-contract

and that's going to return us a thing in JSON. It's able to do this because we have JsonSchema in the macro above the type here:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct EntryResponse {
    pub id: u64,
    pub description: String,
    pub status: Status,
    pub priority: Priority,
}

Something something, this does useful things, including turning your Rust things into a JSON schema. You might notice artifact files in a project that look like this:

├── Cargo.lock
├── Cargo.toml
├── examples (directory)
├── LICENSE
├── NOTICE
├── README.md
├── rustfmt.toml
├── schema // ⚛️ right here ⚛️
│  ├── execute_msg.json
│  ├── instantiate_msg.json
│  └── query_msg.json // ⚛️ lookie here ⚛️
└── src (directory)

See those files in the schema folder? Especially the query_msg.json file?

Those can get turned into TypeScript types using the quicktype package:
https://www.npmjs.com/package/quicktype

This is important because, as we pretended to do a second ago, we can query the smart contract from our terminal, but that's not how we want end users to interact. We'd like to have end users open their browser eventually, and our browser will likely run stuff written in JavaScript, and so we might as well write it in TypeScript, and so we'll want to figure out how to get types.

If we can accomplish this, it'll be ez for anyone to know what the payload should look like when sending to the smart contract, and what the structured response should look like.

The end.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions