Intro
Blockchain technology has revolutionized how we think about decentralized systems, trust, and transparency. Smart contracts have further enhanced the capabilities of blockchains by automating processes that traditionally required third-party intermediaries. These advancements are not just technological milestones but also catalysts for innovation across industries like finance, supply chain, and healthcare.
Why Blockchain and Smart Contracts Matter
At their core, blockchain and smart contracts address fundamental challenges in traditional systems:
Trustless Interactions: Parties can transact without needing to trust each other or an intermediary.
Transparency: Transactions are recorded on an immutable ledger, enhancing accountability.
Efficiency: Automation reduces overhead costs and eliminates bottlenecks.
These features make blockchain and smart contracts critical tools for building next-generation decentralized applications (dApps).
Overview of NEAR Protocol
NEAR Protocol is a modern layer-1 blockchain designed for scalability, user-friendliness, and developer accessibility. Some key advantages of NEAR include:
Sharding for Scalability: Ensures high throughput by dividing the network into smaller, manageable pieces.
Human-Readable Accounts: Makes interacting with blockchain simpler for end users.
Low Fees: Transaction costs are minimal, making it suitable for applications with frequent microtransactions.
Introduction to NEP-141
NEP-141 is the token standard for the NEAR blockchain, similar to ERC-20 on Ethereum. It defines a common interface for token contracts, enabling seamless integration with wallets, dApps, and exchanges. At a high level, NEP-141 provides:
Standardized Methods: Functions for token transfers, balance checks, and allowances.
Cross-Contract Compatibility: Ensures interoperability between various dApps.
Secure Transactions: Built-in safeguards for preventing double-spending and unauthorized transfers.
Technical Tutorial Section
This section walks you through setting up a NEAR development environment, creating a simple NEP-141 smart contract, and integrating it with an Express.js API.
Setting Up the Development Environment
Prerequisites
Node.js (version 18+ recommended)
NEAR CLI: Install using
npm install -g near-cli
NEAR Wallet Account: Create an account at NEAR Wallet.
Steps
NEAR login:
near login
Note : To create a new near account in testnet (Link), For linux server use
save-to-legacy-keychain
instead ofsave-to-keychain
to ensure compatibility while creating wallet accountsInitialize a NEAR Project:
npx create-near-app my-near-app cd my-near-app
Note : during project creation some template options are given (Link)
Smart Contract Project Structure:
hello-near
├── sandbox-test # sandbox testing│ └── main.ava.js
├── src # contract's code
│ └── contract.ts
├── README.md
├── package.json # package manager
└── tsconfig.json
Install Dependencies:
npm install
Compile the Contract:
npm run Build
Basic TypeScript Smart Contract Structure
Here’s a simple NEP-141-compliant smart contract with two functions: one for transferring NEAR and another for transferring a stablecoin.
Contract Code (TypeScript)
import { Context, ContractPromiseBatch, PersistentMap, logging } from "near-sdk-as";
@nearBindgen
export class TokenContract {
private balances: PersistentMap<string, u64> = new PersistentMap<string, u64>("b");
constructor(initialSupply: u64) {
assert(initialSupply > 0, "Initial supply must be positive");
this.balances.set(Context.sender, initialSupply);
}
transferNear(recipient: string, amount: u64): boolean {
assert(recipient.length > 0, "Invalid recipient");
assert(recipient != Context.sender, "Cannot transfer to self");
assert(amount > 0, "Transfer amount must be greater than zero");
assert(Context.accountBalance >= amount, "Insufficient balance");
const promise = ContractPromiseBatch.create(recipient);
promise.transfer(amount);
logging.log(`Transferred ${amount} NEAR to ${recipient}`);
return true;
}
transferToken(recipient: string, amount: u64): boolean {
assert(recipient.length > 0, "Invalid recipient");
assert(recipient != Context.sender, "Cannot transfer to self");
assert(amount > 0, "Transfer amount must be greater than zero");
const sender = Context.sender;
const senderBalance = this.balances.get(sender, 0);
assert(senderBalance >= amount, "Insufficient token balance");
// Update sender balance
this.balances.set(sender, senderBalance - amount);
// Update recipient balance with overflow check
const recipientBalance = this.balances.get(recipient, 0);
assert(
recipientBalance + amount >= recipientBalance,
"Balance overflow"
);
this.balances.set(recipient, recipientBalance + amount);
logging.log(`Transferred ${amount} tokens from ${sender} to ${recipient}`);
return true;
}
@view
getBalance(account: string): u64 {
assert(account.length > 0, "Invalid account");
return this.balances.get(account, 0);
}
}
Deploying the Contract
Build the Contract:
npm run build
Deploy the Contract:
near deploy --accountId <your-account.testnet> --wasmFile build/release/contract.wasm
Testing Strategies for Smart Contracts
Unit Tests: Use NEAR’s AssemblyScript testing framework to verify contract logic.
Integration Tests: Test token transfers between multiple accounts on the testnet.
Edge Cases: Simulate scenarios like insufficient balances or invalid recipient addresses.
Note : For writing test cases for the deployed smart contract (Link)
API Integration with Express.js
Setup Express.js:
npm install express near-api-js body-parser cors axios
API Code:
const express = require('express'); const { connect, keyStores, utils, Contract } = require('near-api-js'); const dotenv = require('dotenv'); const router = express.Router(); // Load environment variables dotenv.config(); // NEAR connection configuration const config = { networkId: 'testnet', keyStore: new keyStores.InMemoryKeyStore(), nodeUrl: 'https://rpc.testnet.near.org', walletUrl: 'https://wallet.testnet.near.org', helperUrl: 'https://helper.testnet.near.org', explorerUrl: 'https://explorer.testnet.near.org' }; // Initialize NEAR connection async function initNearConnection() { const near = await connect(config); const account = await near.account(process.env.NEAR_ACCOUNT_ID); const contract = new Contract( account, process.env.CONTRACT_NAME, { viewMethods: ['getBalance'], changeMethods: ['transferNear', 'transferToken'] } ); return { near, account, contract }; } // Middleware to initialize NEAR connection router.use(async (req, res, next) => { try { const { near, account, contract } = await initNearConnection(); req.near = near; req.account = account; req.contract = contract; next(); } catch (error) { res.status(500).json({ error: 'Failed to initialize NEAR connection' }); } }); // Transfer NEAR tokens router.post('/transfer-near', async (req, res) => { try { const { recipient, amount } = req.body; // Validate input if (!recipient || !amount) { return res.status(400).json({ error: 'Recipient and amount are required' }); } // Convert amount to yoctoNEAR (1 NEAR = 10^24 yoctoNEAR) const amountInYocto = utils.format.parseNearAmount(amount.toString()); // Call the smart contract const result = await req.contract.transferNear({ args: { recipient, amount: amountInYocto }, gas: '300000000000000' // 300 TGas }); res.json({ success: true, transactionHash: result.transaction.hash, amount, recipient }); } catch (error) { console.error('Transfer NEAR error:', error); res.status(500).json({ error: 'Failed to transfer NEAR tokens', details: error.message }); } }); // Transfer custom tokens router.post('/transfer-token', async (req, res) => { try { const { recipient, amount } = req.body; // Validate input if (!recipient || !amount) { return res.status(400).json({ error: 'Recipient and amount are required' }); } // Call the smart contract const result = await req.contract.transferToken({ args: { recipient, amount: amount }, gas: '300000000000000' // 300 TGas }); res.json({ success: true, transactionHash: result.transaction.hash, amount, recipient }); } catch (error) { console.error('Transfer token error:', error); res.status(500).json({ error: 'Failed to transfer tokens', details: error.message }); } }); // Get account balance router.get('/balance/:account', async (req, res) => { try { const { account } = req.params; // Validate input if (!account) { return res.status(400).json({ error: 'Account is required' }); } // Call the smart contract const balance = await req.contract.getBalance({ account: account }); res.json({ account, balance: balance.toString() }); } catch (error) { console.error('Get balance error:', error); res.status(500).json({ error: 'Failed to get balance', details: error.message }); } }); module.exports = router;
Best Practices & Lessons Learned
Smart Contract Security Considerations
Avoid Overflow/Underflow: Use safe arithmetic functions.
Access Control: Ensure only authorized users can call specific functions.
Validate Inputs: Check recipient addresses and transfer amounts.
Testing Approaches
Automate Tests: Use CI/CD pipelines to run contract tests on every commit.
Mock Scenarios: Test real-world scenarios like network congestion.
API Design Patterns
Error Handling: Return meaningful error messages.
Scalability: Design APIs to handle concurrent requests efficiently.
Common Pitfalls to Avoid
Hardcoding Values: Use configuration files for network-specific settings.
Ignoring Gas Costs: Ensure sufficient gas for executing contract functions.
Poor Documentation: Clearly document your API endpoints and contract methods.
Conclusion
This tutorial introduced the NEAR Protocol and its NEP-141 standard, guiding you through creating a simple smart contract and integrating it with an API. By following best practices, you can build robust and scalable dApps on NEAR.