-
Notifications
You must be signed in to change notification settings - Fork 44
DApp Development: Introduction
In this section, we will build an application to store certificates on the Ethereum blockchain. We aim to create a decentralized application for storing and distributing certificates to students who pass various courses.
We will use the technology stack: Node.js for server-side programming, Express for building APIs, Solidity for writing smart contracts, Hardhat as the contract deployment tool, and simulated Ethereum network. Ethers.js library will be used to communicate between the client-side and the blockchain network.
Let's first list all the steps we need to finish coding the DApp.
- Design and write the smart contract.
- Code for the server side.
- Run the Hardhat node.
- Compile and Deploy the contract.
- Coding the server side to interact with the smart contract.
Ethers.js is a JavaScript library used to communicate with the Ethereum blockchain and the contracts deployed in the network. Ethers work by communicating with the modules of the Ethereum client exposed by the JSON-RPC and Web Socket protocols. Ethers can also be used to communicate with the light Ethereum wallet MetaMask.
Modules available in Ethers:
- Providers: Used for connecting to the Ethereum network, providing a concise, consistent interface to standard Ethereum node functionality.
- Signers: Sign messages and transactions and send signed transactions to the Ethereum Network to execute state-changing operations.
- Contract: Allows a simple way to serialize calls and transactions to an on-chain contract and deserialize their results and emitted logs.
- Utilities: Module containing utility functions like conversions, encodings, etc.
We will do a little exercise to understand how the Ethers.js library works. This exercise will show how to use ethers to interact with the Ethereum blockchain. Ethers can be utilized in frontends and backends to read data from the blockchain or make transactions.
The initial step is to include creating a Node.js project.
npm init -y
Install ethers
package.
npm install ethers
Create an 'index.mjs' file in the root directory. Import ethers
package on top.
import { JsonRpcProvider } from 'ethers'
JsonRpcProvider
is a class from ethers
that can create a connection object with a designated node.
const provider = new JsonRpcProvider('http://127.0.0.1:8545')
Before executing the file, we need a blockchain or a simulation running at the machine's 8545 port. We can achieve this by running a Geth node or a Hardhat simulated node.
We can call the getSigner
from the provider
to retrieve a signer in the blockchain. But this returns a Promise. Since we are using ESM, we can use top-level await
to handle the Promise.
const signer = await provider.getSigner()
We log the signer's address into the console for debugging.
console.log('Address:', signer.address)
To view the balance of the address, we should add the following.
const balance = await provider.getBalance(signer.address)
console.log('Balance:', balance)
To get the chain ID of the network, add the following lines to the script.
const network = await provider.getNetwork()
console.log('Chain ID:', network.chainId)
To fetch the latest block number, use the following.
const latestBlock = await provider.getBlockNumber()
console.log('Latest Block:', latestBlock)
Run the file using Node.js.
node index.mjs
Here's the full code.
import { JsonRpcProvider } from 'ethers'
const provider = new JsonRpcProvider('http://127.0.0.1:8545')
const signer = await provider.getSigner()
console.log('Address:', signer.address)
const balance = await provider.getBalance(signer.address)
console.log('Balance:', balance)
const network = await provider.getNetwork()
console.log('Chain ID:', network.chainId)
const latestBlock = await provider.getBlockNumber()
console.log('Latest Block:', latestBlock)
Note: For more ethers APIs, you can refer to ethers documentation.
Deployment is the process of storing a contract in the Ethereum blockchain, which can be executed later. Next, we can look at the steps for deploying a contract and what happens during this.
The steps included are:
- Write the Smart Contract
- Compile
- Deploy
Let's write a simple Storage contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Storage {
string message;
function store(string memory _message) public {
message = _message;
}
function retrieve() public view returns (string memory) {
return message;
}
}
Now, just like we do in other languages, we need to compile this code; we use a library called solc (a JavaScript-based library), which will provide us with the output in a JSON formatted file, which will contain Name, MetaData, ByteCode, ABI, Assembly, etc.
The important ones among these are Bytecode, ABI, and Assembly.
The contract will be stored in the Ethereum network in the form of Bytecode.
When we store the contract in the blockchain, Ethereum will store it and generate a pointer to the stored location, called the contract address; this process is known as contract deployment.
Now that the contract is deployed in the network, how do we interact with it? What we do is create a method call with the name of the function. But how do we know all these input/output parameters of the function that we want to call? We know where the contract is deployed via the contract address, but we can't just go there and look because it's stored as a bytecode that is only readable by the machine. Here is where the Application Binary Interface (ABI) comes into play. The ABI is a JSON formatted string that contains the signature of all the functions that are accessible to an Externally Owned Account. A signature means the name of the function and the details of the input-output parameters of that function.
Now we know how to create a method call, and we also know which contract to interact with using the contract address. So, we create the method call and interact with the contract that is deployed in the network in bytecode form.
Remember, when we compiled, we also got the assembly code equivalent for our Solidity source code. Now, what's the use of this?
In Ethereum, we need to pay a cost for executing a program, specifically for executing a set of instructions. The cost for the transaction is the sum of all these costs. This cost for each instruction is calculated based on the Assembly code; there is a fixed cost defined by the Ethereum community for each instruction, like for STOP is 0, ADD is 3, MUL is 5, and it goes on. The cost will vary according to the complexity of a particular instruction.
Here is the chart of references prepared by the Ethereum community, this is also specified in Ethereum Yellow Paper.
We will connect Remix IDE to the Sepolia network via the MetaMask wallet. Then we will deploy a storage contract to the Sepolia network, discussing the associated transaction costs. Finally, we will interact with the deployed contract.
Next, we will analyze the compilation output of a Solidity smart contract. Then we will analyze the transaction details of a previous contract deployment. Finally, we will load and interact with the deployed contract in Remix.
Create a 'Storage.sol' file in the root folder. Paste the above contract there.
Let's add the 'solc
' package to our existing project. Solc is used to compile the smart contract into ABI and bytecode.
npm install solc
Since we specify the Solidity version as '^0.8.20
', any compiler from '0.8.20' up to the latest patch can compile the contract. Otherwise we need to specify the version of solc
while installing. That is, npm install solc@0.8.20
Now import solc
default package in the 'index.mjs' file.
import solc from 'solc'
Now we need to import our contract. We use readFileSync
from Node.js' inbuilt package 'fs
' for that. Then, we will pass on the location of the contract. Since solc
only accepts JSON strings as arguments for compilation, we also convert the contract to string.
const CONTRACT_FILE = 'Storage.sol'
const content = readFileSync(CONTRACT_FILE).toString()
The input argument of the solidity compiler has a format definition. To compile our smart contract, we need to follow this format.
Check out the input description here.
Hence, we create a variable input
with the language specified as Solidity
, with the content
being the stringified contract. We can describe the output of the solidity compiler based on our needs. Here, we choose all options by providing the wildcard '*
'.
const input = {
language: 'Solidity',
sources: {
[CONTRACT_FILE]: {
content: content,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
}
Then, we JSON.stringify
the input and pass it to the Solidity compiler. What the compiler returns is also a JSON string. So, we use JSON.parse
to convert it to normal JSON. If desired, we can log that output in the console.
const compiled = solc.compile(JSON.stringify(input))
const output = JSON.parse(compiled)
console.log(output)
We are using ethers to deploy the contract to our network. We also require a contract instance for further communication. The ContractFactory
class from ethers
fulfils our needs. So we import it and pass the required parameters (abi, bytecode and a signer/wallet) to it. As for abi
and bytecode
, we can get them from our compiler output. We call the deploy
function from the ContractFactory
object for deployment.
import { ContractFactory } from 'ethers'
const abi = output.contracts[CONTRACT_FILE].Storage.abi
const bytecode = output.contracts[CONTRACT_FILE].Storage.evm.bytecode.object
const factory = new ContractFactory(abi, bytecode, signer)
const contract = await factory.deploy()
What we get in return after calling the deploy
function is a contract instance. Using this contract instance, we can call the functions defined in the contract. Since store
is a write operation which changes a state in the blockchain, it returns a transaction. Invoking store
should be careful as it would cost gas while executing. For retrieval, we call retrieve
, which is a read operation that doesn't cost any gas. We can log these outputs for further debugging.
const trx = await contract.store('Hello, World!')
console.log('Transaction Hash:', trx.hash)
const message = await contract.retrieve()
console.log('Message:', message)
The final code is given below. Simply run the command to execute it.
node index.mjs
import { readFileSync } from 'fs'
import solc from 'solc'
import { JsonRpcProvider, ContractFactory } from 'ethers'
const provider = new JsonRpcProvider('http://127.0.0.1:8545')
const signer = await provider.getSigner()
console.log('Address:', signer.address)
const balance = await provider.getBalance(signer.address)
console.log('Balance:', balance)
const network = await provider.getNetwork()
console.log('Chain ID:', network.chainId)
const latestBlock = await provider.getBlockNumber()
console.log('Latest Block:', latestBlock)
const CONTRACT_FILE = 'contracts/Storage.sol'
const content = readFileSync(CONTRACT_FILE).toString()
const input = {
language: 'Solidity',
sources: {
[CONTRACT_FILE]: {
content: content,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
}
const compiled = solc.compile(JSON.stringify(input))
const output = JSON.parse(compiled)
const abi = output.contracts[CONTRACT_FILE].Storage.abi
const bytecode = output.contracts[CONTRACT_FILE].Storage.evm.bytecode.object
const factory = new ContractFactory(abi, bytecode, signer)
const contract = await factory.deploy()
console.log(contract)
const trx = await contract.store('Hello, KBA!')
console.log('Transaction Hash:', trx.hash)
const message = await contract.retrieve()
console.log('Message:', message)
- Introduction
- Rise of Ethereum
- Ethereum Fundamentals
- DApps & Smart Contracts
- MetaMask Wallet & Ether
- Solidity: Basics
- Solidity: Advanced
- Solidity Use cases and Examples
- DApp Development: Introduction
- DApp Development: Contract
- DApp Development: Hardhat
- DApp Development: Server‐side Communication
- DApp Development: Client-side Communication
- Advanced DApp Concepts: Infura
- Advanced DApp Concepts: WalletConnect
- Event‐driven Testing
- Interacting with the Ethereum Network
- Tokens: Introduction
- Solidity: Best Practises
- Smart Contract Audit
- Ethereum: Advanced Concepts
- Evolution of Ethereum
- Conclusion