A Simple NEP-141 Smart Contract for Crypto Transaction

A Simple NEP-141 Smart Contract for Crypto Transaction

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

  1. NEAR login:

     near login
    

    Note : To create a new near account in testnet (Link), For linux server use save-to-legacy-keychain instead of save-to-keychain to ensure compatibility while creating wallet accounts

  2. Initialize a NEAR Project:

     npx create-near-app my-near-app
     cd my-near-app
    

    Note : during project creation some template options are given (Link)

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

  4. Install Dependencies:

     npm install
    
  5. 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

  1. Build the Contract:

     npm run build
    
  2. 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

  1. Setup Express.js:

     npm install express near-api-js body-parser cors axios
    
  2. 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.