Interaction with Ethereum

Adam Samsonowicz

The goal of this tutorial is to introduce you to tools so you can explore the technology on your own. We will briefly touch every tool, but diving deep into particular one is not in the scope of this article.

Tools introduction

We will work with the following tools

  • Truffle
  • Genache
  • Solidity
  • web3.js
    Solidity is a language for writing smart contractsTruffle is a framework that just makes development easier from tests to contract deployment.Genache is a private/test ethereum network, a network that we will use for interaction
    web3.js is a library that will help us interact with ethereum

Environemt setup

Truffle and solidity

npm install -g truffle

Genache

Just chose the proper way of installation depending on your OS and install it on your PC.

web3

npm install -g web3

Unfortunatelly most of these tools are under development so particular versions may need some extra effort on your part.

Project

The project we will work with is divided into two parts. The first one is contract development and deployment on ganache network. The second one is interacting with the ethereum network from JavaScript side.

Let’s start with the creation of the truffle project.

First you need to create a directory for the truffle project then in the directory execute the following script

truffle init

What you should see is

A brief overview what we see: the contracts directory contains a smart contract written in solidity, migrations directory contains deployment scripts that will be executed during contract migrations that we will see later on, test directory is exactly what its name says and truffle-conffig.js is a configuration file. Pretty simple.

The most intresting part obviously is the smart contract so let’s see what we will deploy on the network.

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Donating {
    enum State {Ongoing, Failed, Succeeded, PaidOut}

    string public name;
    uint256 public targetAmount;
    uint256 public fundingDeadline;
    address payable public beneficiary;
    State public state;
    bool public collected;
    uint256 public totalCollected;

    modifier inState(State expectedState) {
        require(state == expectedState, "Invalid contract state");
        _;
    }

    constructor(
        string memory contractName,
        uint256 targetAmountEth,
        uint256 durationInMin,
        address payable beneficiaryAddress
    ) public {
        name = contractName;
        targetAmount = etherToWei(targetAmountEth);
        fundingDeadline = currentTime() + minutesToSeconds(durationInMin);
        beneficiary = beneficiaryAddress;
        state = State.Ongoing;
    }

    function donate() public payable inState(State.Ongoing) {
        require(isBeforeDeadline(), "No donates after a deadline");
        totalCollected += msg.value;

        if (totalCollected >= targetAmount) {
            collected = true;
        }
    }

    function finishDonating() public inState(State.Ongoing) {
        require(!isBeforeDeadline(), "Cannot finish donates before a deadline");

        if (!collected) {
            state = State.Failed;
        } else {
            state = State.Succeeded;
        }
    }

    function collect() public inState(State.Succeeded) {
        if (beneficiary.send(totalCollected)) {
            state = State.PaidOut;
        } else {
            state = State.Failed;
        }
    }

    function isBeforeDeadline() public view returns (bool) {
        return currentTime() < fundingDeadline;
    }

    function getTotalCollected() public view returns (uint256) {
        return totalCollected;
    }

    function inProgress() public view returns (bool) {
        return state == State.Ongoing || state == State.Succeeded;
    }

    function isSuccessful() public view returns (bool) {
        return state == State.PaidOut;
    }

    function minutesToSeconds(uint timeInMin) internal pure returns(uint) {
        return timeInMin * 1 minutes;
    }

    function etherToWei(uint sumInEth) internal pure returns(uint) {
        return sumInEth * 1 ether;
    }

    function currentTime() internal view returns (uint256) {
        return block.timestamp;
    }
}

Some parts of the language are really simillar to other more popular ones so I will explain few solidity related semantics.

    function isSuccessful() public view returns (bool) {
        return state == State.PaidOut;
    }

The view function makes our function ensure that it will not modify the state.

    function minutesToSeconds(uint timeInMin) internal pure returns(uint) {
        return timeInMin * 1 minutes;
    }

The pure function uses only passed parameters.

    function donate() public payable inState(State.Ongoing) {
        require(isBeforeDeadline(), "No donates after a deadline");
        totalCollected += msg.value;

        if (totalCollected >= targetAmount) {
            collected = true;
        }
    }

Payable is a function modifier that makes the function accept ether when called. Another function modifier is the custom modifier inState().

    modifier inState(State expectedState) {
        require(state == expectedState, "Invalid contract state");
        _;
    }

Modifiers can „add” logic before the „real” function is called. In this example the modifier inState, which checks if the contracts is in the proper state, is called before the function donate_; sign just says that the body of our „real” function will be called.

require checks if the condition is met, if not an exception will be thrown.

Ok as we now know how solidity works let’s move to contract deployment.

Deployment script for smart contract looks like this

var funding = artifacts.require("./Funding.sol");

module.exports = function(deployer) {
  deployer.deploy(
    funding, 
    "j-labs campaign",
    10, //target ammount in eth
    20, //donation window in minutes
    "0xB98be1F8571bC22F07CC1b3c237567533e0cdFe0" //beneficiary account
  );
};

Deploy method accepts funding object followed by its constructor parameters.

Now replace content in truffle-config.js with

module.exports = {
  networks: {
      ganache: {
          host: "localhost",
          port: 7545,
          gas: 5000000,
          network_id: "*"
      }
  }
};

Now open Ganache and run Quickstart workspace. Ten accounts wtih 100 ETH each should be available for you.

As Ganache is running on correct port, please execute following script

truffle migrate --network ganache 

Switch to ganache app and check Transactions tab. You can see that Contract Creation transactions are created which means that contracts are deployed on network.

Now let’s see how we can interact with these contracts using web3 library. Create a new .js file and paste the following content into it.

let fs = require('fs');
let Web3 = require('web3');

let web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider('http://localhost:7545'));

let contractAddress = "0xC31f1b9e5a918Bfa9b3d84d282a5B3cE1ABb4Ca6";
let donatorAddress = "0xDd8784B8a4Ad6a5E18b2A9D4ED98BE2181B982F7";

let abiStr = fs.readFileSync('donate_abi.json', 'utf8');
let abi = JSON.parse(abiStr);

let donate = new web3.eth.Contract(abi, contractAddress);

sendTransaction()
    .then(function() {
        console.log("Success");
    })
    .catch(function(error) {
        console.log(error);
    })

async function sendTransaction() {
    console.log("Getting collected money");
    let collected = await donate.methods.getTotalCollected().call();
    console.log(`Money: ${collected}`)

    console.log("Getting TargetMoney");
    let target = await donate.methods.targetAmount().call();
    console.log(`Money: ${target}`)
}

Interaction with the network is performed via the web3 library, to interact within the contract we have to initialize its instance by calling

let donate = new web3.eth.Contract(abi, contractAddress);

contractAddress is the address of contract on the network.

Which we can retrieve from

Or from the migration output in the console

ABI – The Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction.

It’s simply the interface of the contract thanks to which the developer can interact with the contract.

Okay so how can we get the ABI?

Let’s jump back to our truffle project.

As you can see ABI is a part of the compiled contract.

Please copy the ABI part of the compilation file and paste it to the new file donate_abi.json.

Last but not least, please select one of the accounts in ganache and copy its address to donatorAddress

Now execute the following command to get some information from our deployed contract

node ./yourJsFileName.js

The result should be similar to this

As you can see the value of target ammount doesn’t match the value that was passsed as targetAmmount during contract deployment. The reason is that transactions are executed in wei, the smallest transactional unit in the ethereum network.

eth == 1000000000000000000 wei

Now let’s transfer some ether to our contact

Replace sendTransaction() function with the following content

async function sendTransaction() {
    let ammount = web3.utils.toWei('5', 'ether');
    console.log("Getting collected money");
    let collected = await fund.methods.getTotalCollected().call();
    console.log(`Money: ${collected}`)

    console.log("Getting TargetMoney");
    let target = await fund.methods.targetAmount().call();
    console.log(`Money: ${target}`)

    console.log("Funding the contract")
    await fund.methods.donate().send({from: donatorAddress, value: ammount})

    console.log("Getting collected money after donating");
    collected = await fund.methods.getTotalCollected().call();
    console.log(`Money: ${collected}`)
}

And execute the script in the same way

The result should look like this

Also some intreseting things have happened on the Ganache network

This transaction represents an ether transfer between the donator and contract.

Transfer 5 more ether so we can match the target amount.

After the deadline’s passed we shouldn’t be able to donate any more and an attempt to donate will result in

Now it’s the moment to use finishDonating() function.

async function sendTransaction() {
    let state = await fund.methods.state().call();
    console.log(`State: ${state}`)
    await fund.methods.finishDonating().send({from: donatorAddress});

    state = await fund.methods.state().call();
    console.log(`State: ${state}`)
}

And the result is

If you remember states were defined as enum

enum State {Ongoing, Failed, Succeeded, PaidOut}

As the contract is in the Succeeded state we can transfer eth to the beneficiary account

async function sendTransaction() {
    await fund.methods.collect().send({from: donatorAddress});
}

Now check the beneficiary account in Ganache

This is a proof that our donating contract works correctly

Conclusion

So far we have implemented and deployed a solidity contract on ganache test network. Then with the help of web3 we were able to interact and invoke functions of the contract and change the state of the netowrk. Of course this isn’t the production way of interaction with contracts, so if you found this article intresting I would recommend you to try to build frontend for this app on your own using metamask (a browser extension that helps you interact with network and accounts). The best way to actually learn a new technology is to build something on your own. I hope that this tutorial provided you some tools so you can dive deeper into this topic by yourself.

https://www.trufflesuite.com/

https://web3js.readthedocs.io/en/v1.3.0/

https://app.pluralsight.com/library/courses/ethereum-blockchain-developing-applications

https://docs.soliditylang.org/en/v0.5.16/genindex.html

https://www.sitepoint.com/compiling-smart-contracts-abi/

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami