Skip to main content

Deploying and Interacting with Smart Contracts

In this tutorial, we will walk through the process of deploying a smart contract to a development network, generating the smart contract's ABI and bytecode, and interacting with the smart contract using Web3.js. We will cover the basic concepts of Ethereum, the Solidity smart contract programming language, the Hardhat development environment, and Web3.js.

Overview

Here is a high-level overview of the steps we will be taking in this tutorial:

  1. Review prerequisites
  2. Create a new directory and initialize a new Node.js project
  3. Write the Solidity code for the smart contract and save it to a file
  4. Compile the Solidity code with the Solidity compiler and get its ABI and bytecode
  5. Set up Web3.js and Hardhat
  6. Deploy the smart contract with Web3.js
  7. Use Web3.js to interact with the smart contract
tip

If you encounter any issues while following this guide or have any questions, don't hesitate to seek assistance. Our friendly community is ready to help you out! Join our Discord server and head to the #web3js-general channel to connect with other developers and get the support you need.

Step 1: Prerequisites

This tutorial assumes basic familiarity with the command line as well as familiarity with JavaScript and Node.js. Before starting this tutorial, ensure that Node.js and its package manager, npm, are installed.

$: node -v
# your version may be different, but it's best to use the current stable version
v18.16.1
$: npm -v
9.5.1

Step 2: Create a New Directory and Initialize a New Node.js Project

First, create a new project directory for your project and navigate into it:

mkdir smart-contract-tutorial
cd smart-contract-tutorial

Next, initialize a new Node.js project using npm:

npm init -y

This will create a new package.json file in your project directory.

Step 3: Write the Solidity Code for the Smart Contract and Save It to a File

In this step, we will write the Solidity code for the smart contract and save it as a file in our project directory.

Create a new file called MyContract.sol in your project directory and add the following Solidity code to it:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MyContract {
uint256 public myNumber;

constructor(uint256 _myNumber) {
myNumber = _myNumber;
}

function setMyNumber(uint256 _myNumber) public {
myNumber = _myNumber;
}
}

This simple smart contract defines a myNumber variable that can be set by calling the setMyNumber function.

Step 4: Compile the Solidity Code with the Solidity Compiler and Get Its ABI and Bytecode

In this step, we will use the Solidity compiler (solc) to compile the contract's Solidity code and output the bytecode and ABI that was generated by the Solidity compiler.

First, install the solc package using npm. Make sure that the version of solc is compatible with the version of Solidity that is specified in the smart contract with the pragma solidity directive.

npm i solc@^0.8.0

Next, create a new file called compile.js in your project directory and add the following code to it:

const solc = require('solc');
const path = require('path');
const fs = require('fs');

const contractName = 'MyContract';
const fileName = `${contractName}.sol`;

// Read the Solidity source code from the file system
const contractPath = path.join(__dirname, fileName);
const sourceCode = fs.readFileSync(contractPath, 'utf8');

// solc compiler config
const input = {
language: 'Solidity',
sources: {
[fileName]: {
content: sourceCode,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
};

// Compile the Solidity code using solc
const compiledCode = JSON.parse(solc.compile(JSON.stringify(input)));

// Get the bytecode from the compiled contract
const bytecode = compiledCode.contracts[fileName][contractName].evm.bytecode.object;

// Write the bytecode to a new file
const bytecodePath = path.join(__dirname, 'MyContractBytecode.bin');
fs.writeFileSync(bytecodePath, bytecode);

// Log the compiled contract code to the console
console.log('Contract Bytecode:\n', bytecode);

// Get the ABI from the compiled contract
const abi = compiledCode.contracts[fileName][contractName].abi;

// Write the Contract ABI to a new file
const abiPath = path.join(__dirname, 'MyContractAbi.json');
fs.writeFileSync(abiPath, JSON.stringify(abi, null, '\t'));

// Log the Contract ABI to the console
console.log('Contract ABI:\n', abi);

This code reads the Solidity code from the MyContract.sol file, compiles it using solc, and generates the ABI and bytecode for the smart contract. It then writes the bytecode to a new file called MyContractBytecode.bin and the contract ABI to MyContractAbi.json (these values are also logged to the console).

Run the following command to compile the Solidity code:

node compile.js

If everything is working correctly, you should see the contract's bytecode and ABI logged to the console and see two new files (MyContractAbi.json and MyContractBytecode.bin) in the project directory.

tip

Alternatively, you can generate the bytecode and ABI with npm i solc && npx solcjs MyContract.sol --bin --abi. Keep in mind that this will generate files with different names than those used in this tutorial, so the files would need to be renamed to MyContractBytecode.bin and MyContractAbi.json to follow the rest of this guide exactly as it's written. Learn more about solc-js at https://github.com/ethereum/solc-js.

Step 5: Set Up Web3.js and Hardhat

In this step, we will set up Web3.js and Hardhat.

First, install the required packages with npm:

npm i web3 hardhat

Next, initialize the Hardhat project:

npx hardhat init

Initializing the Hardhat project will require responding to several prompts - select the default option for each prompt. After you initialize the Hardhat project, a number of new files and directories will be created.

To start the Hardhat development network, execute the following command:

npx hardhat node

Executing this command will produce the following output, which provides the URL that can be used to connect to the development network as well as the development network's test accounts:

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: <redacted>

...

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: <redacted>

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

The Hardhat development network needs to remain running in the terminal that was used to start it. Open a new terminal instance in the project directory to execute the remaining commands in this tutorial.

Next, create a new file called index.js in your project directory and add the following code to it:

const { Web3 } = require('web3');

const web3 = new Web3('http://127.0.0.1:8545/');

// Log the chain ID to the console
web3.eth
.getChainId()
.then(result => {
console.log('Chain ID: ' + result);
})
.catch(error => {
console.error(error);
});

This code sets up a Web3.js connection to the Hardhat development network and logs the chain ID to the console.

note

This tutorial uses the default URL for the Hardhat development network (http://127.0.0.1:8545/). Make sure to use the actual URL that was provided when the Hardhat development network was started.

Run the following command to test the connection:

node index.js

If everything is working correctly, you should see the chain ID logged to the console:

Chain ID: 31337

Step 6: Deploy the Smart Contract with Web3.js

In this step, we will use Web3.js to deploy the smart contract to the development network.

Create a file named deploy.js and fill it with the following code:

const { Web3 } = require('web3');
const path = require('path');
const fs = require('fs');

const web3 = new Web3('http://127.0.0.1:8545/');

const bytecodePath = path.join(__dirname, 'MyContractBytecode.bin');
const bytecode = fs.readFileSync(bytecodePath, 'utf8');

const abi = require('./MyContractAbi.json');
const myContract = new web3.eth.Contract(abi);
myContract.handleRevert = true;

async function deploy() {
const providersAccounts = await web3.eth.getAccounts();
const defaultAccount = providersAccounts[0];
console.log('Deployer account:', defaultAccount);

const contractDeployer = myContract.deploy({
data: '0x' + bytecode,
arguments: [1],
});

const gas = await contractDeployer.estimateGas({
from: defaultAccount,
});
console.log('Estimated gas:', gas);

try {
const tx = await contractDeployer.send({
from: defaultAccount,
gas,
gasPrice: 10000000000,
});
console.log('Contract deployed at address: ' + tx.options.address);

const deployedAddressPath = path.join(__dirname, 'MyContractAddress.txt');
fs.writeFileSync(deployedAddressPath, tx.options.address);
} catch (error) {
console.error(error);
}
}

deploy();

This code reads the bytecode from the MyContractBytecode.bin file and the ABI from the MyContractAbi.json file and uses these values to instantiate a new contract object. This example also includes the optional step of estimating the gas that will be used to deploy the smart contract. It then deploys the contract to the development network. Finally, it saves the address of the deployed contract in the MyContractAddress.txt file, which will be used when interacting with the contract.

Run the following command to deploy the smart contract:

node deploy.js

If everything is working correctly, you should see something like the following (note that the values may not be exactly the same):

Deployer account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Estimated gas: 141681n
Contract deployed at address: 0x5FbDB2315678afecb367f032d93F642f64180aa3

Step 7: Use Web3.js to Interact with the Smart Contract

In this step, we will use Web3.js to interact with the smart contract on the development network.

Create a file named interact.js and fill it with the following code:

const { Web3 } = require('web3');
const path = require('path');
const fs = require('fs');

const web3 = new Web3('http://127.0.0.1:8545/');

// Read the contract address from the file system
const deployedAddressPath = path.join(__dirname, 'MyContractAddress.txt');
const deployedAddress = fs.readFileSync(deployedAddressPath, 'utf8');

// Create a new contract object using the ABI and address
const abi = require('./MyContractAbi.json');
const myContract = new web3.eth.Contract(abi, deployedAddress);
myContract.handleRevert = true;

async function interact() {
const accounts = await web3.eth.getAccounts();
const defaultAccount = accounts[0];

try {
// Get the current value of my number
const myNumber = await myContract.methods.myNumber().call();
console.log('myNumber value: ' + myNumber);

// Increment my number
const receipt = await myContract.methods.setMyNumber(BigInt(myNumber) + 1n).send({
from: defaultAccount,
gas: 1000000,
gasPrice: '10000000000',
});
console.log('Transaction Hash: ' + receipt.transactionHash);

// Get the updated value of my number
const myNumberUpdated = await myContract.methods.myNumber().call();
console.log('myNumber updated value: ' + myNumberUpdated);
} catch (error) {
console.error(error);
}
}

interact();

This code uses the previously generated ABI and contract address to instantiate a Contract object for interacting with the MyContract smart contract. It gets the current value of myNumber from MyContract, logs it, updates it, and gets its updated value. It logs the updated myNumber value and the transaction hash to the console.

info

For the purposes of this tutorial, the above script uses relatively arbitrary values for gas and gasPrice when demonstrating how to interact with a smart contract. Refer to the Gas and Priority Fees guide to learn how to properly set these values.

Run the following command to interact with the smart contract:

node interact.js

If everything is working correctly, you should see the current counter value logged to the console, followed by the transaction receipt, and then the updated counter value. The output should look like:

myNumber value: 1
Transaction Hash: 0x956bf08a0ba71a768fdbf9879531e52e9b6a0e2802ad92f66387fc30fa939eb8
myNumber updated value: 2

Conclusion

In this tutorial, you learned how to generate the ABI and bytecode of a smart contract, deploy it to a Hardhat development network, and interact with it using Web3.js.

With this knowledge, you can start experimenting with writing smart contract in order to build decentralized applications (dApps) on the Ethereum network using Web3.js. Keep in mind that this is just the beginning, and there is a lot more to learn about Ethereum and Web3.js - so keep exploring and building, and have fun!

Tips and Best Practices

  • Always test your smart contracts on a development network like Hardhat before deploying them to production.
  • Use the latest version of Web3.js and Solidity to take advantage of the latest features and security patches.
  • Use the Web3.js estimateGas function to estimate the gas required for a transaction before sending it to the network.