Fund Me Smart Contract using Typescript on Near (Non-EVM)

rishabhraghwendra

Rishabhraghwendra18

Posted on October 29, 2023

Fund Me Smart Contract using Typescript on Near (Non-EVM)

Near is a Non-EVM blockchain with 100,000 TPS which is 3x faster, and cheaper than the Non-EVM blockchain Solana!!

On top of that, You can deploy Typescript code as a smart contract on Near!!

Let's build and deploy a fully functional FundMe DApp with Typescript smart contract on Near and learn some new concepts that make Near better than other EVM and non-EVM chains.

Here's a little preview of what we gonna build:

App Preview

This DApp allows users to donate Near tokens, and allows admins to withdraw the donations from the contract from the blockchain explorer.

So let's go 🚀

Setup the development environment

Setup the development environment using the below template. It's made using Vite React and Near CLI which is much faster than the official Near's npx create-near-app CLI.

https://github.com/Rishabhraghwendra18/react-near-template
Enter fullscreen mode Exit fullscreen mode

Just click on the top green button to Use this template which will setup a new repository in your account using the template.

Clone the repository created in your account on your local machine and follow the below instructions:

cd frontend 
npm i
Enter fullscreen mode Exit fullscreen mode

In another terminal:

cd contract
npm i
Enter fullscreen mode Exit fullscreen mode

After installing dependencies in both folders, run npm run deploy in the contract folder to deploy the dummy HelloNear contract.

On the successful deployment of the contract run, npm run dev in the frontend folder.

You will see a basic UI running at http://localhost:5173/ in the browser.

Create a testnet wallet on Near

Create a testnet wallet on Near on MyNearWallet. Near will automatically deposit $200 worth of test Near in your wallet so no need for faucets.

Fun Fact: Near doesn't provide a random hash as your wallet account address. It has named account addresses like mine is rishabh.testnet which is very easy to remember.

Let's now see step by step to build the Typescript smart contract. I will explain to you the meaning of each and every line.

Building the FundMe Contract

Navigate to contract/src/contract.ts file and remove all the code.

Import dependencies on first line:

import { NearBindgen, near, call, view,UnorderedMap } from 'near-sdk-js';
Enter fullscreen mode Exit fullscreen mode
  • NearBindgen: This decorator helps to convert typescript classes to Near smart contracts.
  • near: It has lots of built-in near API like getting contract balances, caller account IDs, etc.
  • call: Decorator for setter functions in contract.
  • view: Decorator for getter functions in the contract.
  • UnorderedMap: An iterable hashmap data structure to store the donation amount with the donor account.

Now create a class with NearBindgen decorator:

@NearBindgen({})
class FundMe {
  beneficiary: string ="<beneficary-account-id>";
  donations= new UnorderedMap<bigint>('d');
}
Enter fullscreen mode Exit fullscreen mode

You may wonder what is 'd' in the UnorderedMap. It's used as a prefix to store key-value pairs in the memory on the near blockchain. So whenever you have two unorderedmap make sure their prefixes are different.

Let's add donate function:

@call({payableFunction:true})
  donate(){
    let donor=near.predecessorAccountId();
    let donationAmount: bigint = near.attachedDeposit() as bigint;

    let donatedSoFar = this.donations.get(donor, {defaultValue: BigInt(0)})
    donatedSoFar+=donationAmount;
    this.donations.set(donor,donatedSoFar);
    near.log(`Thank you ${donor} for donating ${donationAmount}! You donated a total of ${donatedSoFar}`);
  }
Enter fullscreen mode Exit fullscreen mode

Here, we have passed payableFunction:true to decorator because the caller will attach some near amount with function call. It's similar to payable in solidity.

  • near.predecessorAccountId(): To get the caller account ID.
  • near.attachedDeposit(): Near attached to the function call like msg.value in solidity.
  • this.donations.get(): to get the donor account ID and previously donated amount (if any, otherwise default value is 0) from the hashmap.
  • this.donations.set: updating new donation amount for the caller in hashmap.
  • near.log: To log any message on blockchain explorer when transactions complete. Same as console.log in javascript.

Wow! Donation function is complete. Let's move to withdraw_funds function which will allow the admin (the contract deployer) to withdraw the donations from the contract.

@call({privateFunction:true})
  withdraw_funds(){
    let balance=near.accountBalance() - BigInt('10000000000000000000000000');
    const promise = near.promiseBatchCreate(this.beneficiary)
    near.promiseBatchActionTransfer(promise, balance);
  }
Enter fullscreen mode Exit fullscreen mode

Notice the privateFunction:true we have passed to the decorator because we want this function to only be called by the admin.

Private functions work differently in Near than EVM chains.

There's a concept of access keys in Near which is similar to Private keys but are shareable. By default, the user who deploys the contract at first has full access to smart contract functions including private functions. It enables the user to transfer tokens, cross-contract calls, and even update the contract after deployment (we will talk about this in the end).

To know more about new concepts, and cool stuff on Near visit my recent blog for an easy explanation: https://dev.to/rishabhraghwendra/near-blockchain-development-guide-for-ethereum-developers-1f1n

We are also leaving 10 Near in the contract for future use to pay for gas fees in the future:

near.accountBalance() - BigInt('10000000000000000000000000');
Enter fullscreen mode Exit fullscreen mode

Since smart contract is built using Typescript it also has a Promises concept. Every token transfer and cross-contract call returns a promise. In this case, we have like this:

const promise = near.promiseBatchCreate(this.beneficiary)
Enter fullscreen mode Exit fullscreen mode

Remember we can't use async/await in the contract. It's not supported.

To resolve this promise we can do something like this:

near.promiseBatchActionTransfer(promise, balance);
Enter fullscreen mode Exit fullscreen mode

Now let's create the function to get all the donations list

@view({})
  get_all_donations(){
    return this.donations.toArray();
  }
Enter fullscreen mode Exit fullscreen mode

It's a simple view function call that converts the donations hashmap into an array and returns it to the caller.

To get the balance of the contract:

@view({})
  view_balance(){
    return near.accountBalance();
  }
Enter fullscreen mode Exit fullscreen mode

Deploy the smart contract by running npm run deploy in the contract folder. It will also output the transaction ID link on the near test net where the contract has been deployed.

Copy the transaction hash and search it on https://testnet.nearblocks.io/ for better UI.

Note: Whenever you deploy a contract using the above template it will automatically store the contract address in the contract/neardev/dev-account.env environment variable which will be automatically taken up by the frontend.

Now smart contract is done. Since it's not a frontend tutorial I will build a simple frontend and connect it with smart contract.

Building the simple frontend

The frontend is made using Vite React. If your frontend is not running, run it by running npm run dev in the frontend folder.

I am using Material UI to get some UI components. Install it in frontend folder using:

npm install @mui/material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode

Go to App.jsx file and paste below code:

import * as nearAPI from "near-api-js";
import React, { useState } from "react";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Button from "@mui/material/Button";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import "./App.css";

import {  SignInButton,SignOutButton } from "./components/SignInButton.jsx";

const contractId=process.env.CONTACT_NAME;

export default function App({contractId, wallet }) {
  const [donorsList, setDonorsList] = useState([]);
  const [depositValue, setDepositValue] = useState();
  const [totalFunds, setTotalFunds] = useState('');

  // Get blockchian state once on component load
  useEffect(() => {
    getDontations()
      .then((response) => {
        const { utils } = nearAPI;

        let donors = response.map((donor) => ({
          donor: donor[0],
          amount: utils.format.formatNearAmount(donor[1]),
        }));
        setDonorsList(donors);
        console.log("response: ", response);
      })
      .catch(alert)
      getTotalFunds().then((response)=>{
        let funds =nearAPI.utils.format.formatNearAmount(response);
        funds=funds.split(".");
        let wholeNumber = funds[0];
        let decimalPart='0';
        if(funds.length>=2){
          decimalPart=funds[1].padEnd(2,'0').slice(0,2);
        }
        setTotalFunds(wholeNumber+'.'+decimalPart);
      })
  }, []);

  const getTotalFunds = async()=>{
    return wallet.viewMethod({ method: "view_balance", contractId })
  }

  function donate(e) {
    e.preventDefault();
    const deposit = nearAPI.utils.format.parseNearAmount(depositValue);

    // use the wallet to send the funds to the contract
    wallet
      .callMethod({
        method: "donate",
        deposit,
        contractId,
      })
      .then(async () => {
        console.log("Deposited!!");
        return getDontations();
      })
      .then(setValueFromBlockchain)
  }

  function getDontations() {
    // use the wallet to query the all contract's donations
    return wallet.viewMethod({ method: "get_all_donations", contractId });
  }

  return (
    <>
      {wallet.accountId ?<SignOutButton accountId={wallet.accountId} onClick={()=>{wallet.signOut()}}/>:<SignInButton onClick={()=>{wallet.signIn()}}/>}
      <main>
        <div className="flex">
          <h1>Welcome To Fund Me Contract on Near</h1>
          <h2>(Total Funds Raised: {totalFunds} Near)</h2>
          <div className="container-div">
            <div className="txn-list">
              <h3>All Donors</h3>
              <TableContainer component={Paper}>
                <Table
                  sx={{ minWidth: 400 }}
                  size="small"
                  aria-label="a dense table"
                >
                  <TableHead>
                    <TableRow>
                      <TableCell>Donor</TableCell>
                      <TableCell align="right">Amount</TableCell>
                    </TableRow>
                  </TableHead>
                  <TableBody>
                    {donorsList.map((row) => (
                      <TableRow
                        key={row.name}
                        sx={{
                          "&:last-child td, &:last-child th": { border: 0 },
                        }}
                      >
                        <TableCell component="th" scope="row">
                          {row.donor}
                        </TableCell>
                        <TableCell align="right">{row.amount} Near</TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </TableContainer>
            </div>
            <div className="fund-me">
              <h3>Fund Me</h3>
              <Card>
                <CardContent sx={{display:'flex',flexDirection:'column',gap:'1.3rem'}}>
                  <label>Enter Amount</label>
                  <input
                    autoComplete="off"
                    defaultValue={0}
                    className="amount"
                    type="number"
                    onChange={(e)=>setDepositValue(e.target.value)}
                  />
                  <Button variant="contained" size="small" sx={{width:'100%'}} onClick={donate}>Submit</Button>
                </CardContent>
              </Card>
            </div>
          </div>
        </div>
      </main>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The template has the helper class called Wallet in frontend/src/utils/near-wallet.js which has the all methods to call the call function on the contract as well as the view functions.

Checkout main.jsx to know how the template setups the Class object at the app startup.

In App.css paste this:

.flex{
    display: flex;
    gap: 1rem;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.container-div{
    display: flex;
    flex-direction: column-reverse;
    gap: 1rem;
    width: 100%;
    align-items: center;
    justify-content: center;
}
.txn-list{
    width: 100%;
    max-width: 600px;
}
.fund-me{
    width: 100%;
    max-width: 600px;
}
.amount{
    border: 2px solid grey;
    border-radius: 0.3rem;
}
Enter fullscreen mode Exit fullscreen mode

And your first DApp on Near Blockchain is ready!! 🚀

I hope you liked the blog. If you have any doubts let me know in the comments.

To support me, you can donate me some Near on rishabhrag.near 🥳

💖 💪 🙅 🚩
rishabhraghwendra
Rishabhraghwendra18

Posted on October 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related