Account

We'll start by looking at account.rs

The first things we see are the costs:

pub const PIXEL_COST: Balance = 1_000_000_000_000_000_000;
pub const DEFAULT_BALANCE: Balance = 100 * PIXEL_COST;
pub const REWARD_PER_PIXEL_PER_NANOSEC: Balance = PIXEL_COST / (24 * 60 * 60 * 1_000_000_000);
  • The PIXEL_COST is how much it cost to draw a pixel
  • DEFAULT_BALANCE is how much money do you have at the beginning of the game. Berry Club will require you to buy some cucumbers, but this Near Place is give some fake money to you (on testnet) to play with.
  • REWARD_PER_PIXEL_PER_NANOSEC is explaining itself: how much tax is paid to you for the pixels you occupied. The current reward is 1 pixel per day per pixel (see the maths up there).

Then next block

pub type AccountIndex = u32;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct Account {
  pub account_id: AccountId,
  pub account_index: AccountIndex,
  pub balance: u128,
  pub num_pixels: u16,
  pub last_claim_timestamp: u64,
}

These define the Account struct for us. What's worth noting here is, we also decide the types to use. For example, developers usually uses u32 for those defaults unless they need to pump it up. But if you see in the listing above, one can decide to use u16 as well if one think that one will not need that much space.

So that was what one did for num_pixels. If one only have less than 65536 pixels, then one can use u16, however if one need more than that, then one need to up it to u32 or even u64 depending on use cases. So this really depends on your design.

Though why one may want to do this. One hypothesize it might reduce the storage of the output wasm file, maybe. Whether or not it does requires further experimentation.

Looking deeper into the code, these are the things you need for the account. Though one isn't sure why do we need AccountIndex for though, we'll see it in the future and come back to this.

And last_claim_timestamp is really, when was the reward last claimed. So this was originally called claim_timestamp but one thought it might not be clear enough, hence changed the name.

Of course, we have a struct, we would need an implementation, that's the usual way it goes. A struct, and an implementation, (and optionally Default implementation).

impl Account {
    pub fn new(account_id: AccountId, account_index: AccountIndex) -> Self {
      Self {
        account_id,
        account_index,
        balance: DEFAULT_BALANCE.into(),
        num_pixels: 0,
        last_claim_timestamp: env::block_timestamp(),
      }
    }

    pub fn touch(&mut self) {
      let block_timestamp = env::block_timestamp(),
      let time_diff = block_timestamp - self.last_claim_timestamp;
      self.balance += Balance::from(self.num_pixels + 1)
          * Balance::from(time_diff)
          * REWARD_PER_PIXEL_PER_NANOSEC;

      self.last_claim_timestamp = block_timestamp;
    }

    pub fn charge(&mut self, num_pixels: u16) {
      let cost = Balance::from(num_pixels) * PIXEL_COST;
      assert!(
        self.balance >= cost,
        "Not enough balance to draw pixels"
      );
      self.balance -= cost;
    }
}

Usually when we have arguments that we would like to pass in default values (like num_pixels always starts with 0 at the beginning), and there are some other arguments that requires user interaction to pass in certain values (like account_id), we will have a new function. The new function is not that new in smart contract context. If you come from other programming background, you most probably wrote this kind of function yourself in OOP classes. In Python, this is called "class method".

And env::block_timestamp() just uses the block height. If you want to understand more about time, check the Appendix "Bitcoin is Time" in Other Notable Resources for more details about how Bitcoin time is created (which is also used in NEAR).

And we have the touch function. This means, now that we want to claim our rewards. So we know how many REWARD_PER_PIXEL_PER_NANOSEC, and we'll be using that together with the time difference since we last claimed (or since we made our account), to calculate how much rewards we are eligible to claim. This number is put into self.balance. Though one don't know why do we need the + 1 for the num_pixels though.

And then, we reset the self.last_claim_timestamp to the current block_timestamp.

Finally, is charge. Just by looking at the assertion "Not enough balance to draw pixels" we can understand this function is used to draw pixels, and the name of the function suggests this function will charge you a certain cost for drawing the pixels. The code is fairly easy to understand if you read it like storybook.

Note that aside from Number 4 of this site to reduce contract size, if you're using near-sdk-rs version =4.0.0-pre.2 onwards, there's something more lightweight than assert! called require!

use near_sdk::{require};

impl Account {
    // --snip--

    pub fn charge(&mut self, num_pixels: u16) {
      // --snip--
      require!(
        self.balance >= cost,
        "Not enough balance to draw pixels");
      )
      // --snip--
    }
}

Of course, there's some difference in that you need to use format! if you want to print "{}" something that requires variable insertion; while in assert_eq! macro for example you don't need to do that.

Next, we have Place. The struct of Place is in lib.rs rather than here, but one will show you here as well. The explanation will come later, though. Here are just to show you what arguments are being passed in.

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Place {
    pub account_indices: UnorderedMap<AccountId, u32>,
    pub board: board::PixelBoard,
    pub accounts: Vector<Account>,
}

Then now we know the arguments, we shall move on to impl Place without further explanations, for now.

impl Place {
    pub fn get_account_by_id(&self, account_id: AccountId) -> Account {
      let account_index = self
          .account_indices
          .get(&account_id)
          .unwrap_or(self.accounts.len() as u32);

      self.accounts
          .get(u64::from(account_index))
          .map(|mut account| {
            account.touch();
            account
          })
          .unwrap_or_else(|| Account::new(account_id, account_index))
    }

    pub fn get_account_by_index(&self, account_index: AccountIndex) -> Option<Account> {
      self.accounts
          .get(u64::from(account_index))
          .map(|mut account| {
            account.touch();
            account
          })
    }

    pub fn save_account(&mut self, account: &Account) {
      if u64::from(account.account_index) >= self.accounts.len() {
        // it's a newly created account. Save it. 
        self.account_indices
            .insert(&account.account_id, &account.account_index);
        self.accounts.push(account);
      } else {
        // replace to modify state? ONE ISN'T SURE. 
        self.accounts
            .replace(u64::from(account.account_index), account);
      }
    }
}

The first function we look at is get_account_by_id. The name is self-explanatory, but let us discuss a little bit about the code.

Assuming we have a place where we all store the account_index called self.account_indices. This self.account_indices is a Map (also called a dictionary in Python, whatever it is, it's a Key-Value pair storage). So, we can use the .get() method, passing in the "Key", and it'll return the "Value". If we cannot find it, the function panics, then we'll create a new one for it. For the account index, since it is created incrementally, we'll use the final value as the newest account_index. We don't need to + 1 as counting starts from 0 (try imagine the logic yourself by writing it on paper or use a dynamic language programming like Python or Ruby playing around with it to see how it works in real time).

Then we want to return the account that matches the account_id, as the function promises. So, inside the self.accounts, search for the corresponding account, and now we touch the account as we want to change its state, so we need to map it and touch it before returning. If we cannot find the account, we create a new one.

We could choose not to get_account_by_id. We could instead get_account_by_index too. Here, it's a simplification of the previous function; as the previous function did two things: use id to find the index before finding the account. If we already know the index, we don't need the first block of code.

We don't do unwrap_or_else because index is something more "internal" than id. You can have an id passed in, and if it don't exist, you create an account, just like signing up for an account on a website when you first visit it. But the index is more "internal", which we expect that when we use it, it'll be for something internal, and creation of a new Account is not required. So, nobody will use an index for signup. We assume we assign an index to an id after signing up, then the index could be used internally. If we cannot find it, most probably we have some bug in the code rather than requiring to create a new Account, like forgot to delete the index after a user requests Account to be deleted, or we code the logic wrongly hence given the wrong index.

But if you check the Berry Club contract, we actually see Evgeny decided to return Option<Account> later on during the development. So that's another change; perhaps saying that some assumptions are being changed and perhaps it's more optimized that way.

Then if we modify the state, we also need something to save_account. This could be a creation of new account, or perhaps a modification of state to an existing account, whatever it is.

And we have the #[near_bindgen] implementation. For more information about what #[near_bindgen] did, you can find it here.

In brief:

The #[near_bindgen] macro is used on a struct and the function implementations to generate the necessary code to be a valid NEAR contract and expose the intended functions to be able to be called externally. This includes the generation of necessary boilerplate to expose the functions.

#[near_bindgen]
impl Place {
    pub fn get_pixel_cost(&self) -> U128 {
      PIXEL_COST.into()
    }

    pub fn get_account_balance(&self, account_id: ValidAccountId) -> U128 {
      self.get_account_by_id(account_id.into()).balance().into()
    }

    pub fn get_account_num_pixels(&self, account_id: ValidAccountId) -> u16 {
      self.get_account_by_id(account_id.into()).num_pixels
    }

    pub fn get_account_id_by_index(&self, account_index: AccountIndex) -> Option<AccountId> {
      self.accounts
          .get(u64::from(account_index))
          .map(|account| account.account_id)
    }
}

We have four functions, get_pixel_cost, get_account_balance, get_account_num_pixels and get_account_id_by_index. All of these are view functions, and we can easily understand them by their names, so one won't go deeper into it. The logic implementation tries to change the value into something that can be printed out; example perhaps originally it doesn't implement the std::fmt::Display, but calling .into() will change it to a type that does. So that's about it.

Next, we'll take a look at the board, in board.rs.

References