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()) } }