Aha Labs

WIT: Bringing types to NEAR smart contracts

NEAR and WebAssembly

Under the hood, NEAR uses WebAssembly (Wasm) to run smart contracts. While this is great, it has some pain points. For normal Wasm binaries, functions exported can only have float and integer types. So more complex types like strings have to be passed via a pointer, pass_string(len: u32, ptr: u32). This makes it hard to tell from the function signature that a string is being passed; it could be any binary blob.

For NEAR Wasm binaries this problem is made worse because all export functions have the same type signature, foo(), that is they take no arguments and return nothing. The reason for this is because the arguments are serialized in JSON, so the function first asks the host for a binary blob and then deserializes it.

So if you can download a contract’s binary and inspect it, you’ll only have the function names with no other information.

Introducing the wit format

WebAssembly Interface Types seeks to solve the task of passing of more complex types at a low level.

Until this is complete the wit-bindgen project created a .wit format that can be used to generate the needed source ”adapters” to handle passing the complex types.

For example, in JavaScript strings are encoded as UTF-16[note], but UTF-8 in Rust. So passing the binary blob of the string from a Rust Wasm binary is not a simple copy/paste.

For our use case we need remote adapters. Whereas wit-bindgen expects to pass values via a normal function call, we are serializing values on one side of an RPC call and then deserializing them on the other.

witme a CLI tool for generating to and from .wit

So after that introduction let’s walk through a real example. First witme is a Rust binary that can be installed with cargo:

cargo install witme

Assume that you have a Rust smart contract:

use near_sdk::{witgen, near_bindgen}

/// A message that contains some text
#[witgen]
pub struct Message {
  /// Inner string value
  text: String,
}

//...
pub struct Contract {
  message: Message
}

#[near_bindgen]
impl Contract {

  /// A change call to set the message
  pub fn set_message(&mut self, message: Message) {
    self.mesage = message;
  }

  /// A view call to get the current message
  pub fn get_message(self) -> Message {
    self.message
  }
}

The view method get_message returns a Message struct. #[witgen] is a Rust macro that generates a corresponding wit record. See the witgen repo to learn more about generating .wit files from existing code.

Furthermore the #[near_bindgen] macro has been updated to generate function types in wit.

Next you can use witme to generate a .wit file for the contract.

witme near wit

generates index.wit

///  A message that contains some text
record message {
    ///  Inner string value
    text: string
}

///  A change call to set the message
///  change
set-message: function(message: message)

///  A view call to get the current message
get-message: function() -> message

This wit file now describes the Contract’s interface in a language agnostic way. This means we can now generate source code for a different language. For example, TypeScript:

witme near ts

By default this command looks for an index.wit and puts the generated TS in it’s own ./ts folder.

/**
* A message that contains some text
*/
export interface Message {
  /**
  * Inner string value
  */
  text: string;
}

export class Contract {
  /** Account calling the contract and the contractId to call */
  constructor(public account: Account, public readonly contractId: string){}

  /**
  * A change call to set the message
  */
  async set_message(args: {
    message: Message;
  }, options?: ChangeMethodOptions): Promise<void> {}

  /**
  * A view call to get the current message
  */
  get_message(args = {}, options?: ViewFunctionOptions): Promise<Message> {}
}

This means the contract’s interface is now available to use with near-api-js to interact with the contract.

import {Contract, Message} from "message/contract";
import {Account} from "near-api-js";

async function getMessage(currentAccount: Account): Promise<Message> {
  let contract = new Contract(currentAccount, "contract.testnet");
  return contract.get_message();
}

Since the original comments in the rust code are preserved you can also get hover over docs in your IDE, or generate a documentation website (see TenK’s docs for an example).

JSON Schema

Taking this a step further we can generate a json-schema, basically a JSON object that defines the constraints of the data to allow a JSON object to be validated.

witme near json

Currently this is supported by using a ts-json-schema-generator. Though this too could be generated directly from the .wit. This command defaults to find a ./ts/index.ts, which it uses to generate a index.schema.json.

Which would look something like

{
  "GetMessage": {
    "additionalProperties": false,
    "contractMethod": "view",
    "description": "A view call to get the current message",
    "properties": {
      "args": {
        "additionalProperties": false,
        "type": "object"
      }
    },
    "required": [
      "args"
    ],
    "type": "object"
  },
 "SetMessage": {
    "additionalProperties": false,
    "contractMethod": "change",
    "description": "A change call to set the message",
    "properties": {
      "args": {
        "additionalProperties": false,
        "properties": {
          "message": {
            "$ref": "#/definitions/Message"
          }
        },
        "required": [
          "message"
        ],
        "type": "object"
      },
      "options": {
        "additionalProperties": false,
        "properties": {
          "attachedDeposit": {
            "$ref": "#/definitions/Balance",
            "default": "0",
            "description": "Units in yoctoNear"
          },
          "gas": {
            "default": "30000000000000",
            "description": "Units in gas",
            "pattern": "[0-9]+",
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    "required": [
      "args",
      "options"
    ],
    "type": "object"
    },
  "Message": {
    "additionalProperties": false,
    "description": "A message that contains some text",
    "properties": {
      "text": {
        "description": "Inner string value",
        "type": "string"
      }
    },
    "required": [
      "text"
    ],
    "type": "object"
  },
}

First we can see GetMessage is a view function and requires an args object that has no properties. Next SetMessage requires an args object with one field ”message”, the type of which is a reference to the Message type defined in the schema. Since SetMessage is a change function, an options field is also required for how much gas and deposit to attached to the transaction.

Whereas the Typescript would provide compile time checks that the types used in the contract call are valid, this allows the arguments passed to a contract method at runtime to be validated, thus preventing errors before they reach a NEAR node.

Forms for free

Now that we have a schema and know all of the input types, a React form can be autogenerated to validate and interact with the contract. Not only are the types validated, but extra annotations can be used to add additional constraints.

For example, if the text of every message had to start with ”TEXT:” a regular expression can be added to the comments.

/// A message that contains some text
#[witgen]
pub struct Message {
  /// Inner string value
  /// @pattern ^TEXT:
  text: String,
}
/**
* A message that contains some text
*/
export interface Message {
  /**
  * Inner string value
  * @pattern ^TEXT:
  */
  text: string;
}
{
  "Message": {
    "additionalProperties": false,
    "description": "A message that contains some text",
    "properties": {
      "text": {
        "description": "Inner string value",
        "pattern": "^TEXT:",
        "type": "string"
      }
    },
    "required": [
      "text"
    ],
    "type": "object"
  },
}

This is showcased in the TenK repo’s admin panel: https://tenk-dao.github.io/tenk. Try entering ”.” for the account_id and hit submit or check live validation and you’ll get the following error:

.args.account_id should NOT be shorter than 2 characters
.args.account_id should match pattern "^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$"

Future Plans

Our future plans include: adding rust code generation for testing and for making cross contract calls more user friendly; adding borsh support for more efficient serialization than json; adding custom transformations for schema fields, allowing passing values like “10 N” instead of “10000000000000000000000000”; making the form dynamic for targeting different contracts; and lastly creating a wit registry for deployed contracts.

← Back to home