How to create and deploy smart Contract on Near

 

Intermediate

 

How to create and deploy smart Contract on Near

To create and deploy a smart contract on Near, you will need to install Node.js and, if you plan to use Rust as your main language, rustup. Next, you can set up your project using the Near quickstart tool, which will install all necessary packages. Once your project is set up, you can write your smart contract in either Rust or JavaScript using the Near SDK. To deploy your contract, you can use the Near command line interface (CLI) to compile and deploy your contract to the Near network. Make sure to keep track of your contract’s address, as you will need it to interact with your contract.

Prerequisites

To develop any smart contract you will need to you will to install Node.js. If you further want to use Rust as your main language, then you need to install rustup as well.

Node.js

Download and install Node.js. We further recommend to install yarn: npm install -g yarn.

Rust and Wasm

Follow these instructions for setting up Rust. Then, add the wasm32-unknown-unknown toolchain which enables compiling Rust to Web Assembly (wasm), the low-level language used by the NEAR platform.

# Get Rust in linux and MacOS
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env

# Add the wasm toolchain
rustup target add wasm32-unknown-unknown

 

Setting Up Your Project

We recommend you to setup your project using our quickstart tool, since this will install all the necessary packages.

npx create-near-app@latest

 

First Example: A Donation Contract

Let’s look at a simple contract whose main purpose is to allow users to donate $NEAR to a specific account. Particularly, the contract stores a beneficiary account, and exposes a method to give them money while keeping track of the donation.

contract/src/contract.ts

import { NearBindgen, near, call, view, initialize, UnorderedMap } from 'near-sdk-js'
import { assert } from './utils'
import { Donation, STORAGE_COST } from './model'

@NearBindgen({})
class DonationContract {
  beneficiary: string = "v1.faucet.nonofficial.testnet";
  donations: UnorderedMap = new UnorderedMap('map-uid-1');

  @initialize({})
  init({ beneficiary }:{beneficiary: string}) {
    this.beneficiary = beneficiary
  }

  @call({payableFunction: true})
  donate() {
    // Get who is calling the method and how much $NEAR they attached
    let donor = near.predecessorAccountId(); 
    let donationAmount: bigint = near.attachedDeposit() as bigint;

    let donatedSoFar = this.donations.get(donor) === null? BigInt(0) : BigInt(this.donations.get(donor) as string)
    let toTransfer = donationAmount;
 
    // This is the user's first donation, lets register it, which increases storage
    if(donatedSoFar == BigInt(0)) {
      assert(donationAmount > STORAGE_COST, `Attach at least ${STORAGE_COST} yoctoNEAR`);

      // Subtract the storage cost to the amount to transfer
      toTransfer -= STORAGE_COST
    }

    // Persist in storage the amount donated so far
    donatedSoFar += donationAmount
    this.donations.set(donor, donatedSoFar.toString())
    near.log(`Thank you ${donor} for donating ${donationAmount}! You donated a total of ${donatedSoFar}`);

    // Send the money to the beneficiary
    const promise = near.promiseBatchCreate(this.beneficiary)
    near.promiseBatchActionTransfer(promise, toTransfer)

    // Return the total amount donated so far
    return donatedSoFar.toString()
  }

  @call({privateFunction: true})
  change_beneficiary(beneficiary) {
    this.beneficiary = beneficiary;
  }

  @view({})
  get_beneficiary(){ return this.beneficiary }

  @view({})
  number_of_donors() { return this.donations.length }

 

contract/src/model.ts

export const STORAGE_COST: bigint = BigInt("1000000000000000000000")

export class Donation {
  account_id: string;
  total_amount: string;

  constructor({account_id, total_amount}:{account_id: string, total_amount: string}) {
    this.account_id = account_id;
    this.total_amount = total_amount;
  }
}

 

Transfers & Actions

Smart contracts can perform specific Actions such as transferring NEAR, or calling other contracts.

An important property of Actions is that they can be batched together when acting on the same contract. Batched actions act as a unit: they execute in the same receipt, and if any fails, then they all get reverted.

Transfer NEAR Ⓝ

You can send $NEAR from the your contract to any other account on the network. The Gas cost for transferring $NEAR is fixed and is based on the protocol’s genesis config. Currently, it costs ~0.45 TGas.

import { NearContract, NearBindgen, near, call } from 'near-sdk-js'

@NearBindgen
class Contract extends NearContract {
  constructor() { super() }
  
  @call
  transfer({ to, amount }: { to: string, amount: BigInt }) {
    let promise = near.promiseBatchCreate(to)
    near.promiseBatchActionTransfer(promise, amount)
  }
}

 

Function Call

Your smart contract can call methods in another contract. In the snippet bellow we call a method in a deployed Hello NEAR contract, and check if everything went right in the callback.

import { NearContract, NearBindgen, near, call, bytes } from 'near-sdk-js'

const HELLO_NEAR: string = "hello-nearverse.testnet";
const NO_DEPOSIT: number = 0;
const CALL_GAS: bigint = BigInt("5000000000000");

@NearBindgen
class Contract extends NearContract {
  constructor() { super() }

  @call
  call_method() {
    const args = bytes(JSON.stringify({ message: "howdy" }))

    const call = near.promiseBatchCreate(HELLO_NEAR);
    near.promiseBatchActionFunctionCall(call, "set_greeting", args, NO_DEPOSIT, CALL_GAS);

    const then = near.promiseThen(call, near.currentAccountId(), "callback", bytes(JSON.stringify({})), NO_DEPOSIT, CALL_GAS);
    return near.promiseReturn(then);
  }

  @call
  callback() {
    if(near.currentAccountId() !== near.predecessorAccountId()){near.panic("This is a private method")};

    if (near.promiseResultsCount() == BigInt(1)) {
      near.log("Promise was successful!")
      return true
    } else {
      near.log("Promise failed...")
      return false
    }
  }
}

 

Create a Sub Account

Your contract can create direct sub accounts of itself, for example, user.near can create sub.user.near.

Accounts do NOT have control over their sub-accounts, since they have their own keys.

Sub-accounts are simply useful for organizing your accounts (e.g. dao.project.near, token.project.near).

import { NearContract, NearBindgen, near, call } from 'near-sdk-js'

const MIN_STORAGE: bigint = BigInt("1000000000000000000000") // 0.001Ⓝ

@NearBindgen
class Contract extends NearContract {
  constructor() { super() }

  @call
  create({prefix}={prefix: String}) {
    const account_id = `${prefix}.${near.currentAccountId()}`

    const promise = near.promiseBatchCreate(account_id)
    near.promiseBatchActionCreateAccount(promise)
    near.promiseBatchActionTransfer(promise, MIN_STORAGE)
  }
}

 

Creating Other Accounts

Accounts can only create immediate sub-accounts of themselves.

If your contract wants to create a .mainnet or .testnet account, then it needs to call the create_account method of near or testnet root contracts.

import { NearContract, NearBindgen, near, call, bytes } from 'near-sdk-js'

const MIN_STORAGE: bigint = BigInt("1820000000000000000000"); //0.00182Ⓝ
const CALL_GAS: bigint = BigInt("28000000000000");

@NearBindgen
class Contract extends NearContract {
  constructor() { super() }

  @call
  create_account({account_id, public_key}={account_id: String, public_key: String}) {
    const args = bytes(JSON.stringify({ 
      "new_account_id": account_id,
      "new_public_key": public_key 
    }))

    const call = near.promiseBatchCreate("testnet");
    near.promiseBatchActionFunctionCall(call, "create_account", args, MIN_STORAGE, CALL_GAS);
  }
}

 

Deploy a Contract

When creating an account you can also batch the action of deploying a contract to it. Note that for this, you will need to pre-load the byte-code you want to deploy in your contract.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{near_bindgen, env, Promise, Balance};

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Contract { }

const MIN_STORAGE: Balance = 1_100_000_000_000_000_000_000_000; //1.1Ⓝ
const HELLO_CODE: &[u8] = include_bytes!("./hello.wasm");

#[near_bindgen]
impl Contract {
  pub fn create_hello(&self, prefix: String){
    let account_id = prefix + "." + &env::current_account_id().to_string();
    Promise::new(account_id.parse().unwrap())
    .create_account()
    .transfer(MIN_STORAGE)
    .deploy_contract(HELLO_CODE.to_vec());
  }
}

 

Add Keys

When you use actions to create a new account, the created account does not have any access keys, meaning that it cannot sign transactions (e.g. to update its contract, delete itself, transfer money).

There are two options for adding keys to the account:

  • add_access_key: adds a key that can only call specific methods on a specified contract.

  • add_full_access_key: adds a key that has full access to the account.

import { NearContract, NearBindgen, near, call } from 'near-sdk-js'

const MIN_STORAGE: bigint = BigInt("1000000000000000000000") // 0.001Ⓝ

@NearBindgen
class Contract extends NearContract {
  constructor() { super() }

  @call
  create_hello({prefix, public_key}={prefix: String, public_key: String}) {
    const account_id = `${prefix}.${near.currentAccountId()}`

    const promise = near.promiseBatchCreate(account_id)
    near.promiseBatchActionCreateAccount(promise)
    near.promiseBatchActionTransfer(promise, MIN_STORAGE)
    near.promiseBatchActionAddKeyWithFullAccess(promise, public_key.toString(), 0)
  }
}

 

Notice that what you actually add is a “public key”. Whoever holds its private counterpart, i.e. the private-key, will be able to use the newly access key.

Delete Account

There are two scenarios in which you can use the delete_account action:

  • As the last action in a chain of batched actions.

  • To make your smart contract delete its own account.

import { NearContract, NearBindgen, near, call } from 'near-sdk-js'

const MIN_STORAGE: bigint = BigInt("1000000000000000000000") // 0.001Ⓝ

@NearBindgen
class Contract extends NearContract {
  constructor() { super() }

  @call
  create_delete({prefix, beneficiary}={prefix: String, beneficiary: String}) {
    const account_id = `${prefix}.${near.currentAccountId()}`

    const promise = near.promiseBatchCreate(account_id)
    near.promiseBatchActionCreateAccount(promise)
    near.promiseBatchActionTransfer(promise, MIN_STORAGE)
    near.promiseBatchActionDeleteAccount(promise, beneficiary.toString())
  }

  @call
  self_delete({beneficiary}={beneficiary: String}) {
    const promise = near.promiseBatchCreate(near.currentAccountId())
    near.promiseBatchActionDeleteAccount(promise, beneficiary.toString())
  }
}