Interaction with Ethereum
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.
1 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.
Links
https://web3js.readthedocs.io/en/v1.3.0/
https://app.pluralsight.com/library/courses/ethereum-blockchain-developing-applications