Building a Decentralized Application (DApp) on the Ethereum Blockchain With JavaScript and Solidity



Building a Decentralized Application (DApp) on the Ethereum Blockchain With JavaScript and Solidity

DApps are applications that run on a decentralized peer-to-peer network, as opposed to a client-server network, and are governed by all the members, as opposed to a single entity that centrally owns the underlying servers and databases. In Ethereum, a dapp is backed by what is known as a “smart contract” containing the logic of the dapp. This makes a dapp running on the Ethereum blockchain potentially unstoppable as it won’t go down because the logic is replicated across many peers in the network. The smart contract backing our dapp is written in Solidity, an object-oriented, high-level language for implementing smart contracts on blockchain platforms. Our dapp will run locally on Ganache which we will interact with via a Node.js application using the web3.js library, a JavaScript library that provides an API to interact with an Ethereum blockchain.

We start by setting up our project.

$ mkdir ethereum-simple-decentralized-application
$ cd ethereum-simple-decentralized-application
$ echo "{ \"name\": \"ethereum-simple-decentralized-application\", \"version\": \"1.0.0\", \"description\": \"A decentralized application (dapp) containing a smart contract deployed to an in-memory blockchain.\" }" >> package.json

We create a directory to hold our project and add a package.json file to it as we will be using the Node package manager for our project’s dependencies.

Launching our in-memory blockchain with Ganache

Instead of developing the dapp against a live blockchain, we will use Ganache: a personal, in-memory Ethereum blockchain. The team behind Ganache has also built Ganache CLI which we will use to interact with our in-memory Ethereum blockchain via the command line. Ganache CLI is written in JavaScript making it easy and familiar for us web developers to use.

The first thing that we need to do is to install the ganache-cli package in our project which we do using a familiar tool: the Node package manager (npm).

$ nvm use 12 && npm install ganache-cli

Executing this command will create a node_modules folder and a package-lock.json file in our project’s main directory.

$ ls
node_modules package-lock.json package.json

Now let’s launch Ganache CLI to see what has been installed.

$ node_modules/.bin/ganache-cliGanache CLI v6.10.1 (ganache-core: 2.11.2)
Available Accounts
(0) 0x496B1E7794494E1ff44D036925728186Af03dDb6 (100 ETH)
(1) 0x14aCF29C8B33034e9E459218126cafC1a1E7ed93 (100 ETH)
(2) 0x00D59D87d9DFd7a89FA541715e2d998349f10364 (100 ETH)
(3) 0xaDd0A44f0E7aE577d177Ed23dA5449301E8401F6 (100 ETH)
(4) 0xc0a30F28697BBCf1ED569012D9cF9Dddb1b6444F (100 ETH)
(5) 0xa360C829083D5dFc381A913e13B9f4A6756B6a8f (100 ETH)
(6) 0x0B86E07440f8AE6Fbb325FD043cc9b0585c53FD7 (100 ETH)
(7) 0x4fb9f2ceF1B85b5774509FCE4025396D3886BC4d (100 ETH)
(8) 0x51b4d3BB8ECc292b71c8435D648Be9dDaf2A36c8 (100 ETH)
(9) 0x3f90e9DBafB8E1Ab1fa250A9eb7cF126955C34ec (100 ETH)Private Keys
(0) 0x8a24d3f9313c02046999dc9191c29372e3aea28de03b5a01be11f559a38d4e73
(1) 0x8bf1573b7e5fa11eb4b2a3134667b317f53852580062fc264bf1863160b8390a
(2) 0xdbff66e07fbae8ddca541eb114cf8879f1e9cbc95334deb40b6105033d172fe5
(3) 0xf7306aadef15ea421113f7e95d200e8b33ccfaef0745127ed1e553066b20beac
(4) 0x1734b721539638b191e981b621337864bb934d6927ab847b6e609f6c4fa9a702
(5) 0xd545c1fcb2d29371c0c447529ead316af7ed86f50b97592f1b78a7b2fd35c127
(6) 0x90b597304632a200debe347c2aeeddc5012789fa626052e25ab6bf1d3dd697f3
(7) 0x89159cde33199b0da9b8a317e3e92f243efe38fc76045556e60e56baced2ba95
(8) 0xdb1f97689085844ed68fd69ca9d844a86f2e027104432a84ce0443fab4597ee8
(9) 0x6f4b0937d948c11d70d81331a86421866542c2e47054fa0bc0e2b0c1fb815f20HD Wallet
Mnemonic: picture imitate fan snow tray fabric grass fish glance rare remove nuclear
Base HD Path: m/44'/60'/0'/0/{account_index}Gas Price
20000000000Gas Limit
6721975Call Gas Limit
9007199254740991Listening on

Launching Ganache CLI automatically creates ten test accounts and preloads them with 100 units of fake ether (ETH), by default, however this amount can be changed using the --defaultBalanceEther option. Ether is the currency for transactions performed on the Ethereum blockchain so each of these accounts can be thought of as bank accounts, or “wallets” in blockchain terminology.

In addition to units of fake ether, the Ganache instance also defines a “Gas Price” (gasPrice), “Gas Limit” (gasLimit), and “Call Gas Limit” (callGasLimit). The concept of “gas” in Ethereum is a measurement of the computational effort that is needed in order to commit the transaction to the Ethereum network. This differentiates computational cost from cryptocurrency cost much like we differentiate the fuel cost to run a car from the currency cost of purchasing that fuel.

The smart contract that we will write in this blog consists of a series of operations that make up a transaction that will become a new block in the blockchain. Ethereum miners provide the processing power to process and verify our transactions and ultimately add the new blocks by quickly solving complex hashing algorithms. Miners are incentivized to provide their computational services through rewards in the form of a transaction fee which is the product of the gas price and gas usage. Having a transaction fee ensures that the network does not get misused because each operation that it performs comes at a cost.

The Gas Price that we see as the output of launching Ganache CLI is the price per gas unit, in units of wei, which is used to calculate the total transaction fee if a transaction does not set its own gas price. By default, the value of Gas Price is set to 20000000000 wei, however this amount can be changed using the --gasPrice option when launching Ganache CLI. Wei is the smallest unit of ether; wei is to ether like a penny is to a dollar. In real-terms, it takes 10¹⁸ wei to equal 1 ether.

The Gas Limit referred to here is the block gas limit (as opposed to the transaction gas limit that we will see later in this blog). Rather than setting a fixed limit, the size of a block in the Ethereum blockchain is bound by the total units of gas that can be spent per block so that the number of transactions that can fit into a block varies. Ethereum miners determine what this limit should be by signalling it to the network each block. Miners are given this ability because changes to the block gas limit affects the resources necessary to effectively mine Ethereum. Ganache sets the gas limit to a hexadecimal value of 0x6691b7 (or a decimal value of 6,721,975) by default, however this amount can be changed using the --gasLimit option when launching Ganache CLI.

We calculate the total transaction fee by multiplying the gas price by the sum of the units of gas used in a transaction, i.e. gas usage. We will see examples of gas usage later in this blog when we start creating transactions.

The Call Gas Limit is the maximum amount of gas units that can be expended to execute specific calls: eth_call and eth_estimateGas. Ganache sets the gas limit to a hexadecimal value of 0x6691b7 (or a decimal value of 9,007,199,254,740,991) by default, however this amount can be changed using the --callGasLimit option when launching Ganache CLI. We will see examples of these calls later in this blog.

Writing the smart contract with Solidity

Now that we have an in-memory blockchain up and running, it is time to write the smart contract! We will write the smart contract using the Solidity programming language: an object-oriented, high-level language for implementing smart contracts. But first we need to set up our project accordingly.

$ touch SmartContract.sol
$ npm install solc@0.6.4

We create the .sol file where we define the smart contract. In addition, we install the solc package containing the Solidity compiler. Solidity is a statically-typed programming language which will eventually be compiled down to bytecode for execution on an Ethereum Virtual Machine (EVM).

With the necessary files and packages in place, we write a simple smart contract.

pragma solidity ^0.6.4;contract SmartContract {
mapping (bytes32 => uint256) public likesReceived;
bytes32[] public postList;  constructor(bytes32[] memory postNames) public {
    postList = postNames;
  }  function totalLikesFor(bytes32 post) view public returns (uint256)
    return likesReceived[post];
  }  function likePost(bytes32 post) public {
    likesReceived[post] += 1;

The very first thing that we do is use the pragma keyword to dictate which version of the Solidity compiler should be used to compile this code. We then create an object of the contract type which is similar to a class in that this object will contain the characteristics and methods of our smart contract. Within the class, we define a variable of the mapping type which is equivalent to an associative array or hash where the key is the name of a blog post stored as the Bytes32 data type and the value is an unsigned integer storing the number of “likes” that post received. We then define an array which we will fill with items of the Bytes32 type as Solidity doesn’t currently allow us to pass in an array of strings in the constructor for a contract object.

We next define the constructor for our smart contract which is called when the smart contract is deployed to Ethereum. When we deploy the contract, we will pass an array of post names.

We then define a totalLikesFor function which returns the total “likes” that a post has received so far.

We then define a likePost function which increments the “like” count for the given post.

Now we are ready to compile the code. To do so, we run an executable script provided by the solc package.

$ node_modules/.bin/solcjs --bin --abi SmartContract.sol
$ lsSmartContract.sol

The Solidity compiler will produce two files upon successful compilation. The first file is the SmartContract_sol_SmartContract.abi file. The .abi file extension indicates that it is an Application Binary Interface which is the standard way in Ethereum for users outside the blockchain and for other smart contracts to interact with this smart contract. Our SmartContract_sol_SmartContract.abi file describes what methods are available in our smart contract to any user of the contract. The second file is the SmartContract_sol_SmartContract.bin file containing the bytecode we get when the source code in our SmartContract.sol file is compiled. This is the code which will be deployed to Ethereum.

Deploying the smart contract with the web3.js library

Now that we have written and compiled our smart contract, it is time to deploy it to our in-memory Ethereum blockchain Ganache! In order to interact with Ganache, we will use the web3.js library. This library is provided by Ethereum to connect a dapp to a blockchain node enabling us to interact with Ethereum via our favorite JavaScript framework like React.js and start building. Let’s install the package containing this library now.

$ npm install web3

Let’s build and test a connection to our blockchain node provided by Ganache.

$ node
> Web3 = require('web3');
> connection = new Web3('');
> connection.eth.getAccounts().then(console.log);[
]> connection.eth.getBalance('0x496B1E7794494E1ff44D036925728186Af03dDb6').then(console.log);100000000000000000000

With Ganache CLI running in a different terminal, we open up a new terminal, start up a local Node.js server, and load the web3 package containing the web3.js library. We define the connection variable to reference a new instance of the package’s main class which contains all things related to Ethereum. We can see that the connection was successful as the getAccounts method provided by web3 returns the accounts that we created in the Launching our in-memory blockchain with Ganache section above. In addition, we can see the current balance of any account using the getBalance method which takes a String of the address of the account from which we want to get the balance and returns the current balance in wei. The returned value of 100000000000000000000 wei is equivalent to 100 ether which you will remember is what Ganache preloaded into our accounts.

When we call the getAccounts and getBalance methods from our Nodes.js server, we will see the following output in Ganache CLI.


These are the underlying JSON-RPC methods that are being called by the methods that we are calling via the web3.js library. JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol that uses JSON as its data format. This gives us a good example of how web3.js is giving us a convenient interface for the RPC methods. Without the web3.js library, our calls would like something like this.

curl -X POST — data '{“jsonrpc”:”2.0",”method”:”eth_accounts”}' ''
curl -X POST — data '{“jsonrpc”:”2.0",”method”:”eth_getBalance”,”params”:[“0x496B1E7794494E1ff44D036925728186Af03dDb6”, “latest”],”id”:1}' ''

As you can see, web3.js has a number of benefits to this approach. The first benefit being the advantage of asynchronous, Promise-based calling and the second being the returned data is scoped down to just the data that we are looking for and in our desired format.

Now that we have a successful connection to a blockchain node, we can deploy our smart contract to Ethereum. We can do so using the web3.js library and the .abi and .bin files that were created during compilation.

> abi = JSON.parse(fs.readFileSync('SmartContract_sol_SmartContract.abi'));
> contract = new connection.eth.Contract(abi);

To deploy the contract, we will use the deploy method provided by the web3.js library. This method is available to a Contract object so we first need to create a new instance of that object. The constructor of a Contract requires us to pass a jsonInterface object as a parameter. Luckily we already have such an interface in the .abi file, so all that we need to do is to create a new variable that we will arbitrarily name abi, read the SmartContract_sol_SmartContract.abi file into our Node.js server, and assign the returned object to our new variable. We then pass this to the Contract constructor.

> bytecode = fs.readFileSync('SmartContract_sol_SmartContract.bin').toString();
> listOfPosts = ['A Beginner’s Guide to Ethereum','How Does Ethereum Work?','The Year in Ethereum','What is Ethereum 2.0?', 'Ethereum is a Dark Forest'];
> contract.deploy({
  data: bytecode,
  arguments: [ =>
    from: '0x496B1E7794494E1ff44D036925728186Af03dDb6',
    gasPrice: connection.utils.toWei('0.00003', 'ether'),
    gas: 1500000
  }).then((deployedContract) => {
  contract.options.address = deployedContract.options.address;

The deploy method requires a data parameter containing the bytecode of the smart contract in String format. Luckily we already have the bytecode format of our smart contract in the .bin file, so all that we need to do is to create a new variable that we will arbitrarily name bytecode, read the SmartContract_sol_SmartContract.bin file into our Node.js server, and convert the returned Buffer to a String. Next we define the new listOfPosts variable and populate it with a list of post names. We do this because, if you recall from the Writing the smart contract with Solidity section above, we defined a constructor for our smart contract that expects an Array of post names. It will get this from the arguments parameter that we pass to the deploy method.

Now we are ready to deploy our smart contract! We pass to the deploy method the stringified bytecode and the Array of post names. Recall from the Writing the smart contract with Solidity section above that we told our smart contract to expect the list of post names in Bytes32 format so we first convert our Array of Strings using the asciiToHex utility function provided by the web3.js library. The deploy method returns the transaction object which has a send method.

We then call the send method of the transaction object to deploy the smart contract to Ethereum. This method requires a from parameter containing the address the transaction should be sent from. We got such an address when we launched Ganache using Ganache CLI which returned ten account numbers; we can pass any of these account numbers as an argument.

The send method takes an optional gasPrice argument that defines the gas price in wei to use for this transaction. If we didn’t explicitly set this price then the transaction would use the default gas price that was defined when we launched Ganache CLI. The total transaction cost will then be converted to ether and deducted from the account passed to in the from parameter to buy the gas. We discussed gas price in the Launching our in-memory blockchain with Ganache section above. Recall from that section that the gas price is the incentive for miners on Ethereum to process our transactions so a high or low gas price affects the speed at which a transaction is executed. One example where one might set a higher gas price is in the presale of tokens of an initial coin offering (ICO) to increase the chances of including the transaction in the next block. Vice versa, one met set a lower gas price when transferring funds from one wallet to another. To determine the gas prices of a faster transaction versus a slower transaction, you can consult various online services such as ETH Gas Station.

In addition to the gasPrice argument, we also pass the optional gas argument to define the maximum units of gas that we are willing to expend on this transaction, i.e. the transaction gas limit. The transaction gas limit is different than the block gas limit that we defined in the Launching our in-memory blockchain with Ganache section above. Recall from that section that we set the block gas limit to 6,721,975 units of gas, so with a transaction gas limit of 1,500,000 units of gas we could fit four of these transactions into one block of our Ethereum blockchain. If the smart contract is successfully deployed, the send method will return a Promise which resolves with the newly deployed contract. We assign our contract variable the number of the account that we defined in the deployment of the smart contract so that we can interact with it later.

When we execute the code above we should see the following in Ganache CLI.

Transaction: 0xfd6770811adecbcbc736b8cac99614b4ed89d58adae51742f02befbe0ab29c20
Contract created: 0x315520e2aa049a49c2ef63c29671fab8432f5d9e
Gas usage: 307612
Block Number: 1
Block Time: Sun Sep 06 2020 10:28:58 GMT-0400 (Eastern Daylight Time)

The total units of gas used was 307,612 which was under the limit that we set of 1,500,000 units of gas.

We can get the current balance, in wei, of any of our accounts using the getBalance method provided by the web3 library.

> connection.eth.getBalance('0x496B1E7794494E1ff44D036925728186Af03dDb6').then(console.log);

We set a price per unit of 30,000,000,000,000 wei (0.00003 ether). The transaction used 307,612 units of gas. So the total transaction cost was 9,228,360,000,000,000,000 wei (9.22836 ether). If we take our initial account balance of 100,000,000,000,000,000,000 wei (100 ether) and subtract the total transaction cost of 9,228,360,000,000,000,000 wei (9.22836 ether) the difference is 90,771,640,000,000,000,000 wei (90.77164 ether) which aligns with what we see returned by the getBalance method.

Using the smart contract with Node.js

Now we can interact with our newly deployed smart contract via our Node.js console. We can access the two methods defined in our contract, totalLikesFor and likePost, using the methods method that the web3.js library provides to instances of the Contract class. Let’s first check the number of likes for one of our posts.

> contract.methods.totalLikesFor(connection.utils.asciiToHex('How Does Ethereum Work?')).call(console.log);null 0

We convert one of our post names to the Bytes32 type as per the parameters that we defined in our totalLikesFor function in the Writing the smart contract with Solidity section above. We then execute this function using the methods method. As expected, the returned value is 0 since we just deployed our smart contract and no posts have yet been “liked”.

Let’s change that next by “liking” that post by executing the likePost function of our smart contract.

> contract.methods.likePost(connection.utils.asciiToHex('How Does Ethereum Work?')).send({from: '0x14aCF29C8B33034e9E459218126cafC1a1E7ed93'}).then((response) => console.log(response));{
  transactionHash: '0x7958f89a74ba6efd27a8a76b66e1e7b4d2649c1bd402d0344a3433f344eb3dfe',
  transactionIndex: 0,
  blockHash: '0xe0ee74688653cb998311cd592eacacdcd78b3bee5165c319db2c8e0d3b03b311',
  blockNumber: 2,
  from: '0x14acf29c8b33034e9e459218126cafc1a1e7ed93',
  to: '0x315520e2aa049a49c2ef63c29671fab8432f5d9e',
  gasUsed: 42673,
  cumulativeGasUsed: 42673,
  contractAddress: null,
  status: true,
  logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
  events: {}

Like we did for the totalLikesFor function, we convert one of our post names to the Bytes32 type before calling our likePost function using the methods method. We then execute this function and pass as an argument the address of one of the other accounts that was created for us when we launched Ganache CLI. The returned response contains the receipt of the transaction where we can see that a second block was formed, and we can determine from which account the transaction came from as well as how many units of gas were used to execute the transaction.

We can see that our likePost function worked as expected by executing our totalLikesFor function on the target post.

> contract.methods.totalLikesFor(connection.utils.asciiToHex('How Does Ethereum Work?')).call(console.log);
null 0

As expected, the returned value is now 1 as per the instructions that we coded into the smart contract.

Let’s pause here to note that we use the send method to execute our likePost function while we used the call method to execute our totalLikesFor function. This exemplifies an important distinction between executing functions in Ethereum. The call method can not modify the smart contract state and therefore can be executed in Ethereum without sending a transaction. In contrast, the send method can modify the state of the smart contract and therefore will send a transaction. This is important because, as we have discussed throughout this blog, transactions come at a cost because they add a new block to the blockchain.

When writing smart contracts with Solidity, we can use keywords in the declaration of our functions to determine whether or not they will modify the state of the smart contract. Recall from the Writing the smart contract with Solidity section above that we declared the totalLikesFor function as follows.

function totalLikesFor(bytes32 post) view public returns (uint256) {
  return likesReceived[post];

The view keyword in this declaration ensures that this function will not modify the state of the smart contract. Users of the smart contract should look for these keywords in the code of the smart contract to avoid executing view and similar functions with the costly send method rather than the free call method. One means to determine this is via the .abi file. Recall from the Writing the smart contract with Solidity section above that the .abi file is the interface for a smart contract. As such, we can inspect this file to determine which functions can modify the state and which cannot. The web3.js library provides the jsonInterface method to instances of the Contract class for such inspection.

> contract.options.jsonInterface;[
    inputs: [ [Object] ],
    name: ‘likePost’,
    outputs: [],
    stateMutability: ‘nonpayable’,
    type: ‘function’,
    constant: undefined,
    payable: undefined,
    signature: ‘0x68dbf9a8’
    inputs: [ [Object] ],
    name: ‘totalLikesFor’,
    outputs: [ [Object] ],
    stateMutability: ‘view’,
    type: ‘function’,
    constant: true,
    payable: undefined,
    signature: ‘0x333f5f0b’

We can see that the totalLikesFor function has a stateMutability value of ‘view’ which aligns with how we defined the function. The function also has a constant value of true indicating that the function is specified to not modify the state of the smart contract.

We can verify that executing the likePost function, a function that does modify the state of the smart contract, via the send method indeed sent and charged us for a transaction using the getBalance function to check the balance of the account from which we initiated the transaction.

> connection.eth.getBalance( '0x14aCF29C8B33034e9E459218126cafC1a1E7ed93').then(console.log);

Prior to the transaction, our account was full.

> connection.eth.getBalance( '0x14aCF29C8B33034e9E459218126cafC1a1E7ed93').then(console.log);

Our account balance is now lower. Looking at the receipt of the transaction, which we can access at any time via the getTransactionReceipt method provided by the web3.js library, we see that the transaction used 42,673 units of gas. Since we didn’t define a gas price in the send method, the default gas price of 20,000,000,000 wei (0.00000002 ether) is used which makes the total transaction cost equal to 853,460,000,000,000 wei (0.00085346 ether). This aligns with our remaining account balance of 99,999,146,540,000,000,000 (99.99914654 ether) that we see above. We can also see the transactionHash value which represents the unique ID for this transaction. Rather than overriding the transaction before it, each transaction adds a new block to the chain generating a new transactionHash which can be used to reference this transaction at any point in the future. This makes each transaction immutable and allows anyone to find our previous transaction by looking it up the blockchain’s ledger with its ID. This immutability and accessible insight is one of the big advantages of blockchains such as Ethereum.

Great work! We now have a decentralized application running on an in-memory Ethereum blockchain backed by a smart contract. To retrospect on the work that we did in this blog, we first set up and launched a personal, in-memory blockchain using Ganache and Ganache CLI. Next, we wrote a smart contract using the Solidity programming language and compiled it using Solidity Compiler. We then deployed our smart contract using the web3.js JavaScript-based library creating the first block in the Ethereum blockchain. Lastly, we interacted with the newly deployed smart contract by executing its coded terms, creating new transactions, and adding new blocks to the blockchain.