Basic Counter Example

At its core, the NEAR blockchain is a big database. A contract is a program with its own storage space, commonly called "contract storage" or "on-chain storage." You have access to read and write to this storage like you would any key/value data structure.

The simplest example of this is a counter. A contract that can read, increment, and decrement a value.

To follow along, use VS Code to open the contracts/counter folder inside the raen-examples project that you cloned in Getting Set Up. Then open src/lib.rs. We'll walk through this code in order.

Docs & Imports

The \\! lines at the top are container documentation comments you can use to document a whole contract/module/crate.

The use lines import dependencies, like import or require in JavaScript. Look in Cargo.toml to see that kebab-case crate names like near-sdk are turned into snake-case names like near_sdk in the code. Let's learn more about these specific imports by seeing how they're used.

State Shape

Rust's type system gives us a way to tell NEAR's Runtime how our contract's state is shaped. If you're following along, look for the struct definition on line 10:

pub struct Counter {
    val: i8,
}

About this:

ThingExplanation
structA data structure with fields, similar to a class, object, Hash, or Dictionary in other languages. This struct is named Counter; you could name it whatever you want.
valThis is a variable name; you could change it (using the F2 key in VS Code) to anything else you want, such as counter or number or num.
i8val is of type i8, a signed 8-bit integer. This means it can be between -128 and 127.

For more on Rust's number types, check out The Rust Book's description. For more on why i8 goes from negative 128 but only to positive 127, search for primers about how numbers get stored as binary.
pubShort for public, meaning that the item is visible outside of the file. You need this on the struct you intend to store in contract storage.

Technically pub is a namespace, but that's not important right now.

Ok, straightforward enough.

But if you're actually looking at the code in your own editor, you're probably wondering about the stuff right above pub struct Counter:

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
    val: i8,
}

These are called macros. Macros are Rust's way of generating extra code. This kind of "hash bracket" macro (#[...]) will expand the definition of the thing that follows, adding extra logic to save you from needing to copy-paste boilerplate.

(The other kind of Rust macro looks like a function, but has an exclamation mark, like log!(...). You'll see this soon.)

Here's what these macros are doing:

ThingExplanation
near_bindgenGenerates bindings for NEAR. This is how you tell the NEAR Runtime that this is the struct you want to put in contract storage.
derive(...)The derive attribute. Takes a list of derive macros, and generates extra code for each one.
DefaultGenerates a default value for the struct. If you check the rest of the file, you'll see that we never initialize Counter or val. But when you deploy the contract, you'll see that val defaults to 0.
BorshSerializeGenerates boilerplate for taking your in-memory contract data and turning it into borsh-serialized bytes to store on-disk.
BorshDeserializeGenerates boilerplate for reading borsh-serialized, on-disk bytes and turning them into in-memory data structures.
Expand this section to see the code the above derive macro will generate.
impl ::core::default::Default for Counter {
    #[inline]
    fn default() -> Counter {
        Counter {
            val: ::core::default::Default::default(),
        }
    }
}
impl borsh::de::BorshDeserialize for Counter
where
    i8: borsh::BorshDeserialize,
{
    fn deserialize(buf: &mut &[u8]) -> ::core::result::Result<Self, borsh::maybestd::io::Error> {
        Ok(Self {
            val: borsh::BorshDeserialize::deserialize(buf)?,
        })
    }
}
impl borsh::ser::BorshSerialize for Counter
where
    i8: borsh::ser::BorshSerialize,
{
    fn serialize<W: borsh::maybestd::io::Write>(
        &self,
        writer: &mut W,
    ) -> ::core::result::Result<(), borsh::maybestd::io::Error> {
        borsh::BorshSerialize::serialize(&self.val, writer)?;
        Ok(())
    }
}

Borsh?

The data in your contract and, indeed, your contract itself, need to be stored on-disk in between calls to your contract.

Then when a NEAR user makes a call to your contract, validators' computers need to fetch your contract from storage, load it into memory, check how your contract's storage is shaped, then finally deserialize your contract storage and execute the call.

Borsh is a serialization specification (a way of representing data as bytes) developed and maintained by the nearcore team, because nothing else was good enough when NEAR started. The biggest problem with other serialization standards was a lack of consistency—the same in-memory object could be represented in multiple valid ways as bytes.

You'll see Borsh used in a couple different ways in NEAR contracts. The most common is here—as the serialization format for your contract data.

Methods

To attach methods to a struct in Rust, use an impl.

A View Method

Let's start by looking at the view method starting on line 17:

impl Counter {
    /// Public method: Returns the counter value.
    pub fn get_num(&self) -> i8 {
        self.val
    }
    ...
}
ThingExplanation
///In Rust, you can use // for regular comments and /// for documentation comments, which will automatically be turned into documentation by various tooling, including RAEN.
fnThe keyword for function
&selfA reference to the Counter struct. This lets you access the fields on the struct, however, it is immutable meaning that you cannot change or "mutate" the field.

The ampersand (&) means that the get_num function borrows self, rather than taking ownership of self. To feel confident with Rust, you will need to understand borrowing and ownership. For now, pay attention to when this guide uses or omits the ampersand on variable names, and rely on Rust's industry-leading error messages to help you figure out if and when you need an ampersand in your own code.
-> i8The return type of the method. A signed 8-bit integer, matching val.
self.valThis accesses the field val. You will also notice that there is no return statement. This is because in Rust everything is an expression, and a function returns the value of the last expression, as long as there is no semicolon after it.

Go ahead and play with this a bit. Do things that don't make sense, and your well-configured editor will yell at you. Looking at the errors will teach you about how Rust works. You can try each of these things, changing it back to how it was after each:

  • Change -> i8 to -> i16
  • Change -> i8 to -> (), an empty tuple
  • Remove -> i18 – note that you get the same error message as when you changed it to an empty tuple! Empty tuple is one of the closest things Rust has to a void type.
  • Change self.val to self.val;
  • Change self.val to return self.val; and it will behave the same way as self.val with no semicolon. Note that this is super non-idiomatic! No one writes Rust this way. Get used to paying attention to the presence & lack of semicolons.

A Change Method

Next let's see how to mutate or change val. Look at the increment method starting on line 22:

impl Counter {
    ...

    /// Public method: Increment the counter.
    pub fn increment(&mut self) -> i8 {
        self.val += 1;
        log!("Increased number to {}", self.val);
        self.val
    }
  
    ...
}
ThingExplanation
;Rust uses semicolons to separate expressions. See that the last line here is the same as the only line in get_num, with no semicolon. You may have already guessed that this means set_num also returns the value of num. This also matches the return type, -> i8.
&mut selfHere the reference to the struct is defined as mutable, allowing it to be changed.

Mutable references are covered in that same chapter of the Rust Book on borrowing and ownership.
self.val += 1;The same as self.val = self.val + 1. This mutates the Counter struct. Note that the new value will not be saved to contract storage until the method call is over. If you have a bug in the log! line or before the return line, the new value will not actually be persisted.
log!This is a function-like macro, as mentioned above. log! generates code that formats a string and emits a log which you can see in NEAR Explorer or other block-explorer tools.

Digging Deeper: Result Serialization

Look again at the view function get_num. It returns an i8. However, any call to a NEAR contract happens via Remote Procedure Calls, and this i8 needs to be serialized into something the consumer can read. By default, near_bindgen generates code to serialize it into a JSON number, since this is the most common case, e.g. a web app calling get_num. If you really need to, you can override this setting. For example, this would serialize it to Borsh instead:

#[result_serializer(borsh)]
pub fn get_num(&self) -> i8 {
    self.val
}

Serializing to Borsh can be useful in a couple cases:

  • gas-intensive contract methods that need to optimize every detail
  • methods only used in cross-contract call logic, where no external consumers are expected or allowed

However, most of the time you will probably want to stick with the default JSON result serialization, to make it easier for apps to interact with your contract.

Summary

In this unit we saw our first NEAR smart contract. We were introduced to some Rust basics, like its type system, numbers, struct, documentation comments, and (im)mutability. We saw how NEAR works with Rust to generate code for you using macros, leaving your code clean and focused on your own logic, rather than various boilerplate.

Next, we'll build the contract with raen and interact with it using RAEN Admin.