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 astruct
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
.