author

Kien Duong

May 7, 2022

Donation Smart Contract with Ethereum (Part 2)

1. Rinkeby network

In the previous blog, we’ve presented an overview about a smart contract, how to deploy it on the Remix tool. You can follow it at here. In this article, we’re going to show you how to build a smart contract by using NodeJS. You’ve already known that we have three steps to deploy a smart contract to Ethereum network

  • Writing the smart contract by using Solidity language.
  • Compile Solidity file.
  • Deploy to Ethereum network.

Basically, we also have to do three steps like those when building by NodeJS. Create .sol file for writing Solidity code, use solc-js library to compile solidity code, Web3 & HDWalletProvider to deploy the contract to Ethereum network. Before deploying to the real Ethereum mainnet, we must develop our DApps in a testnet environment. Rinkeby is one of Ethereum testnet environments. It is a fork of the Ethereum mainnet.

In order to deploy the smart contract to Rinkeby network, we can use APIs of a third party. In this article, we’re going to use Infura APIs. You must go to the Infura site & register a new account to get the Rinkeby endpoint with a token.

donation smart contract 12

2. Deployment solution

In the previous article, we’ve gone into create a single donation smart contract. In reality, a platform must provide the ability to create multiple smart contracts. It means that our platform will have many smart contracts and each one belongs to a manager.

We have a technical issue at here that, a smart contract can be deployed from anywhere, from anyone and each contract will be deployed to a specific address. It means that a normal user will be able to deploy a smart contract & get back the deployed address.

About the security, we cannot provide the code to a normal user. We cannot deploy each smart contract as well because we will have to pay the cost for each contract. The idea is that we need to provide a feature that allows a user can create a donation smart contract on our platform & he will pay the cost for this deployment. In order to solve this problem, we will need to create a new Factory contract that can deploy Donation contract source code.

donation smart contract 13

  • createDonation is called when a user wants to create a new donation contract & he will become the manager of this contract.
  • getDeployedDonations is called by the platform, return list deployed donation contracts.

    // Donation.sol
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Factory {

    address[] public deployedDonations;

    function createDonation(uint minimum) public {
        address donation = address(new Donation(minimum, msg.sender));
        deployedDonations.push(donation);
    }

    function getDeployedDonations() public view returns (address[] memory) {
        return deployedDonations;
    }

}

contract Donation {

    struct Request {
        string description;
        uint value;
        address recipient;
        bool complete;
        uint approvalsCount;
        mapping(address => bool) approvals;
    }

    address public manager;
    uint public minimumContribution;
    mapping(address => bool) public approvers;
    mapping(uint => Request) public requests;
    uint public requestsCount;
    uint public approversCount;

    modifier isManager() {
        require(msg.sender == manager);
        _;
    }

    constructor(uint minimum, address creator) {
        minimumContribution = minimum;
        manager = creator;
    }

    function contribute() public payable {
        require(msg.value > minimumContribution);
        if (!approvers[msg.sender]) {
            approvers[msg.sender] = true;
            approversCount++;
        }
    }

    function createRequest(string memory description, uint value, address recipient) public isManager {
        Request storage rq = requests[requestsCount++];
        rq.description = description;
        rq.value = value;
        rq.recipient = recipient;
        rq.complete = false;
        rq.approvalsCount = 0;
    }

    function approveRequest(uint index) public {
        Request storage request = requests[index];
        require(approvers[msg.sender]);
        require(!request.approvals[msg.sender]);
        request.approvals[msg.sender] = true;
        request.approvalsCount++;
    }

    function finishRequest(uint index) public isManager {
        Request storage request = requests[index];
        require(request.approvalsCount > (approversCount / 2));
        require(!request.complete);
        payable(request.recipient).transfer(request.value);
        request.complete = true;
    }

    function getDetail() public view returns (
        address, uint, uint, uint, uint
    ) {
        return (
            manager,
            minimumContribution,
            requestsCount,
            approversCount,
            address(this).balance
        );
    }

}
  

3. Backend workflow

We will create a basic ExpressJS project to process Solidity contract and return data to frontend. After compiling the Solidity contract, we will get the ABI information and go to the deployment step to Rinkeby network. When the Solidity contract is deployed successfully, we will be able to get the deployed address and store it to database.

donation smart contract 14

Here is the structure for backend side:

donation smart contract 15

    // compile.js
const path = require('path');
const solc = require('solc');
const fs = require('fs-extra');

const buildPath = path.resolve(__dirname, 'build');
if (buildPath) {
    fs.removeSync(buildPath);
}

const contractPath = path.resolve(__dirname, 'contracts', 'Donation.sol');
const source = fs.readFileSync(contractPath, 'UTF-8');

const input = {
    language: 'Solidity',
    sources: {
        'Donation.sol': {
            content: source
        }
    },
    settings: {
        outputSelection: {
            '*': {
                '*': ['*']
            }
        }
    }
};

const output = JSON.parse(solc.compile(JSON.stringify(input)));

fs.ensureDirSync(buildPath);

for (let contractName in output.contracts['Donation.sol']) {
    fs.outputJsonSync(
        path.resolve(buildPath, `${contractName}.json`),
        output.contracts['Donation.sol'][contractName]
    );
}
  

Using solc library to compile Solidity code. The system will read the content of Solidity file inside the contracts folder. The compiled output will be stored in the build folder.

    // deploy.js
require('dotenv').config({ path: '../../.env' });

const HDWalletProvider = require('@truffle/hdwallet-provider');
const Web3 = require('web3');
const deployAddressModel = require('../models/deployedAddress');
const compiledFactory = require('./build/Factory.json');
const abi = compiledFactory.abi;
const bytecode = compiledFactory.evm.bytecode.object;

const provider = new HDWalletProvider({
    mnemonic: {
        phrase: process.env.ETHEREUM_MNEMONIC
    },
    providerOrUrl: process.env.ETHEREUM_URL
});
const web3 = new Web3(provider);

const deploy = async () => {
    try {
        const accounts = await web3.eth.getAccounts();
        const result = await new web3.eth.Contract(abi).deploy({ data: bytecode }).send({ gas: '3000000', from: accounts[0] });

        await deployAddressModel.create({
            account: accounts[0],
            address: result.options.address,
            created_at: new Date(),
            updated_at: new Date(),
        });

        console.log('Deploy successfully');
        provider.engine.stop();
    } catch (e) {
        console.log(e);
    }
};
deploy();
  

In order to deploy a smart contract to Rinkeby network, we must have two params. The first one is the mnemonic data that was generated when setup Metamask and the second one is the Rinkeby url that has been got from Infura platform.

Using Truffle provider & Web3 to deploy to Rinkeby network, the result of deployment process is the contract address. We must store it to the DB, remember that every time we deploy the contract we will get a new address.

The next step, we need to create a new /contract endpoint to return the latest deployed address & ABI data to frontend.

    // contract.js
const express = require('express');
const router = express.Router();
const status = require('../constants/statusCode');
const deployAddressModel = require('../models/deployedAddress');
const compiledDonation = require('../ethereum/build/Donation.json');
const compiledFactory = require('../ethereum/build/Factory.json');

router.get('/', async (req, res) => {
    try {
        const result = await deployAddressModel.latest();
        return res.status(status.OK).json({
            address: result.address,
            factory: {
                abi: compiledFactory.abi,
            },
            donation: {
                abi: compiledDonation.abi,
            },
        });
    } catch (e) {
        console.log(e);
    }
});

module.exports = router;
  

Recent Blogs