Interacting with Ethereum Smart Contracts, Directly from Solidity Source Code

--

TLDR: This is a tutorial for getting started with a web3.js plugin called web3-plugin-craftsman which compiles a smart contract and generates its ABI and Bytecode and enables the developer to use them smoothly or save them for later use.

Introduction

In the world of blockchain development, the direct interaction with Solidity smart contracts from source code often presents a daunting task for beginners and even some experienced developers. The web3-plugin-craftsman package aims to simplify this process using its ExtendedContract class, so that you can focus more on the development side. By the end of this tutorial, we would have walked through the creation, compilation, deployment, and interaction with a simple Solidity smart contract directly from a Solidity source code. Get ready to enhance your smart contract development experience!

Note: 📝 web3.js version 4 supports plugin to extend functionality. And web3-plugin-craftsman package is a plugin that allows instantiating contracts directly from Solidity source code or a solidity file. So, instead of requiring the Bytecode from compiling the smart contract somewhere, you can pass the source code, or the file path, to ExtendedContract and then use it as you would use a normal web3.js contract object. And you can also write the generated contract's ABI and Bytecode to a TypeScript, or a JavaScript, file that is compatible and easily readable by a normal web3.js Contract.

Prerequisites

Before we begin, please make sure you have:

  1. A basic understanding of JavaScript and Solidity.
  2. Node.js installed on your personal computer. Node.js can be downloaded from the official Node.js website.
  3. A package manager for Node like npm or yarn
  4. A local Ethereum development node working on your system. This will act as our local blockchain. If you do not have one already, you may download and use Ganache from http://truffleframework.com/ganache

Section 1: Setting Up a JavaScript Project

Let’s set up our JavaScript project. Follow the steps below:

  1. Start by creating a new directory for your project and navigate into it:
mkdir my-ethereum-project && cd my-ethereum-project

2. Initiate a new Node.js project:

npm init -y

This will create a package.json file in our project directory.

3. Create an index.js file in your project's root directory:

touch index.js

4. In your package.json file, set your project type to module. This is needed to be able to use the same code samples provided in this tutorial:

"type": "module",

5. Install web3.js and web3-plugin-craftsman to interact with Ethereum:

npm install web3 web3-plugin-craftsman

Or if you are using yarn:

yarn add web3 web3-plugin-craftsman

With these steps, you have successfully prepared a JavaScript Node.js project. The directory structure of your project should look as follows:

my-ethereum-project
┣ node_modules
┣ index.js
┣ package.json
┗ package-lock.json

You’re now ready to write your first Ethereum contract using Solidity and JavaScript!

Section 2: Creating Your First Solidity Smart Contract

Alright, our workspace is set up and ready. Now, let’s get our hands dirty by coding a simple HelloWorld smart contract in Solidity.

Below is the Solidity code:

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

contract HelloWorld {
string public message;
constructor(string memory initMessage) {
message = initMessage;
}
function updateMessage(string memory newMessage) public {
message = newMessage;
}
}

In this contract, we have a public string variable message that we initialize at the contract deployment using constructor. We also have a function updateMessage to change the message afterward.

Go ahead and save this code into a file called HelloWorld.sol. After saving the file, our web3-plugin-craftsman package will use this file to create our smart contract in the next steps.

Section 3: Deploy and Interact with your contract

This section is about the JavaScript code that will compile, deploy, read, and update state variables in your Smart Contract. And it consists of 5 simple steps.

Step 1: Initialize web3 object and register the web3-plugin-craftsman plugin

In this section, we will initialize our web3 object and make it ready. Append the following to your index.js file:

import { ExtendedWeb3 } from 'web3-plugin-craftsman';
import { Web3 } from 'web3';

// Initialize a new Web3 object
// The string should be the path to your local Ethereum RPC URL
const web3 = new Web3('http://localhost:7545');
// Creating and Register the ExtendedWeb3 plugin
web3.registerPlugin(new ExtendedWeb3());

On executing the above commands, we first create an instance of the Web3 object that can connect to our local Ethereum development node. And then we register a new instance of ExtendedWeb3 as a plugin to the created web3 instance.

Step 2: Creating the Contract Instance Directly from Solidity Code

With our HelloWorld Solidity contract ready, let's utilize web3-plugin-craftsman to create a JavaScript instance of our contract. Append the following to your index.js file:

// Create an ExtendedContract
const contract = new web3.craftsman.ExtendedContract('./HelloWorld.sol');

If you did not save HelloWorld.sol at the same directory where your index.jsexists, replace './HelloWorld.sol' with the exact path where you saved your file.

This command will create an instance of our HelloWorld contract. Our ExtendedContract object, contract, now represents the HelloWorld contract in our Javascript environment. We can interact with it using different methods provided by web3-plugin-craftsman.

Congratulations! You’ve successfully set up your tools and created your first smart contract using web3-plugin-craftsman. In the upcoming sections, we'll explore how to interact with this contract and utilize the benefits of web3-plugin-craftsman.

Step 3: Handling Compilation Results

Now that we’ve created our contract instance, we should handle the compilation result. As the compilation starts to happen instantly after the contract source code is provided in the constructor, we need to wait for the compilation result. Here’s how it is done. Append the following to your index.js file:

// Wait for the contract compilation and handle compilation errors, if any
try {
const compilationResult = await contract.compilationResult;
console.log(
'Compilation was successful!\n',
JSON.stringify(compilationResult, undefined, ' '),
);
// the compilationResult will consists of:
// {
// abi: ContractAbi,
// bytecodeString: string,
// contractName: string,
// }
} catch (error) {
console.error('Compilation failed', error);
}

In case of a successful compilation, the compilationResult will include the bytecode, the application binary interface (ABI), and the contract name given inside the Solidity file. This is available to you as you might need that information later.

Note: If the contract compilation fails (due to any syntax errors or other problems), an error will be thrown with details about the problems which helps debug and fix them.

Now, you can run the file with what we had written, in it till now, by calling:

node index.js

And the output would be:

Compilation was successful!
{
"abi": [
{
"inputs": [
{
"internalType": "string",
"name": "initMessage",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "message",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "newMessage",
"type": "string"
}
],
"name": "updateMessage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecodeString": "608060405234801561001057600080fd5b506040516107283803806107288339818101604052810190610032919061015a565b806000908051906020019061004892919061004f565b50506102bf565b82805461005b9061022f565b90600052602060002090601f01602090048101928261007d57600085556100c4565b82601f1061009657805160ff19168380011785556100c4565b828001600101855582156100c4579182015b828111156100c35782518255916020019190600101906100a8565b5b5090506100d191906100d5565b5090565b5b808211156100ee5760008160009055506001016100d6565b5090565b6000610105610100846101cc565b61019b565b90508281526020810184848401111561011d57600080fd5b6101288482856101fc565b509392505050565b600082601f83011261014157600080fd5b81516101518482602086016100f2565b91505092915050565b60006020828403121561016c57600080fd5b600082015167ffffffffffffffff81111561018657600080fd5b61019284828501610130565b91505092915050565b6000604051905081810181811067ffffffffffffffff821117156101c2576101c1610290565b5b8060405250919050565b600067ffffffffffffffff8211156101e7576101e6610290565b5b601f19601f8301169050602081019050919050565b60005b8381101561021a5780820151818401526020810190506101ff565b83811115610229576000848401525b50505050565b6000600282049050600182168061024757607f821691505b6020821081141561025b5761025a610261565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61045a806102ce6000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80631923be241461003b578063e21f37ce14610057575b600080fd5b61005560048036038101906100509190610228565b610075565b005b61005f61008f565b60405161006c91906102a2565b60405180910390f35b806000908051906020019061008b92919061011d565b5050565b6000805461009c90610383565b80601f01602080910402602001604051908101604052809291908181526020018280546100c890610383565b80156101155780601f106100ea57610100808354040283529160200191610115565b820191906000526020600020905b8154815290600101906020018083116100f857829003601f168201915b505050505081565b82805461012990610383565b90600052602060002090601f01602090048101928261014b5760008555610192565b82601f1061016457805160ff1916838001178555610192565b82800160010185558215610192579182015b82811115610191578251825591602001919060010190610176565b5b50905061019f91906101a3565b5090565b5b808211156101bc5760008160009055506001016101a4565b5090565b60006101d36101ce846102f5565b6102c4565b9050828152602081018484840111156101eb57600080fd5b6101f6848285610341565b509392505050565b600082601f83011261020f57600080fd5b813561021f8482602086016101c0565b91505092915050565b60006020828403121561023a57600080fd5b600082013567ffffffffffffffff81111561025457600080fd5b610260848285016101fe565b91505092915050565b600061027482610325565b61027e8185610330565b935061028e818560208601610350565b61029781610413565b840191505092915050565b600060208201905081810360008301526102bc8184610269565b905092915050565b6000604051905081810181811067ffffffffffffffff821117156102eb576102ea6103e4565b5b8060405250919050565b600067ffffffffffffffff8211156103105761030f6103e4565b5b601f19601f8301169050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b8381101561036e578082015181840152602081019050610353565b8381111561037d576000848401525b50505050565b6000600282049050600182168061039b57607f821691505b602082108114156103af576103ae6103b5565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fea264697066735822122082c59b20c4e3d2fe1910e039581a1734d393c358be11bda90acf0f4394bfa60c64736f6c63430008000033",
"contractName": "HelloWorld"
}

Step 4: Deploying the Smart Contract

Having successfully compiled our HelloWorld contract, it's now time to deploy it to our local Ethereum network.

Let’s begin by setting up the address which will deploy the contract along with the initial parameter — the message.

Replace <account_address> with the account address that will be used for deploying the contract and <initial_message> with your initialization message. Append the following to your index.js file:

// get the accounts provided by your Ethereum node (like Ganache).
const accounts = await web3.eth.getAccounts();
const fromAddress = accounts[0];

// Set up the constructor parameter
const initial_message = '<initial_message>';
const deployParams = { arguments: [initial_message], from: fromAddress };

And, let’s add the code for deployment by appending the following to your index.js file:

// Deploy the contract
let contractInstance;
try {
const deployFunction = contract.deploy(deployParams);
const gas = await deployFunction.estimateGas();
contractInstance = await deployFunction.send({from: fromAddress, gas});
console.log(
'\nThe contract has been deployed to address:\n ',
contractInstance.options.address,
);
} catch (error) {
console.error('Deployment failed', error);
}

Now, you can run the file by calling:

node index.js

And the output should have something like this at the end of the printed log:

The contract has been deployed to address: 
0xCFd6bF9105E7c273d8046A7EdAf3A4B95De27F3c

If the deployment is successful, contractInstance is now an instance of HelloWorld contract on the blockchain. It can be used to interact with the contract.

In case the deployment fails (due to issues like lack of funds in the deployer account, output bytecode exceeding Ethereum’s block gas limit, etc.), an error will be thrown.

Be sure to have a connection to a development node. That you enter in the code at step 1. If you use Ganache GUI, on your machine on its the default port, the url would be `http://localhost:7545'.

If you reach here without issues, then congratulations! Your HelloWorld contract is now deployed! In the next sections, we'll show you how to interact with it.

Step 5: Interacting with the Smart Contract

We can interact with the contract by calling the methods we had defined within it. Let’s begin with the code for reading the initial message we passed to the contract. We can do this by appending this code to index.js file:

// Read the current value of the message
let currentMessage;
try {
currentMessage = await contractInstance.methods.message().call();
console.log('Initial message:', currentMessage);
} catch (error) {
console.error('Failed to fetch message:', error);
}

Next, here is the code to update the message with a new one. We can append this code to the same file:

// Update the message
const newMessage = 'Hello, Ethereum!';
const updateParams = { from: fromAddress };
try {
await contractInstance.methods.updateMessage(newMessage).send(updateParams);
console.log('Message updated successfully!');
} catch (error) {
console.error('Failed to update message:', error);
}

And finally, we can verify that our message has been updated successfully with this piece of code, which also can be appended toindex.js file:

// Verify the message has been updated
try {
currentMessage = await contractInstance.methods.message().call();
console.log('Updated message:', currentMessage);
} catch (error) {
console.error('Failed to fetch updated message:', error);
}

Sum up code and console output

After finishing the above steps. You should already have the following inside your index.js:

import { ExtendedWeb3 } from 'web3-plugin-craftsman';
import { Web3 } from 'web3';

// Initialize a new Web3 object
// The string should be the path to your local Ethereum RPC URL
const web3 = new Web3('http://localhost:7545');
// Creating and Register the ExtendedWeb3 plugin
web3.registerPlugin(new ExtendedWeb3());

// Create an ExtendedContract
const contract = new web3.craftsman.ExtendedContract('./HelloWorld.sol');

// Wait for the contract compilation and handle compilation errors, if any
try {
const compilationResult = await contract.compilationResult;
console.log(
'Compilation was successful!\n',
JSON.stringify(compilationResult, undefined, ' '),
);
// the compilationResult will consists of:
// {
// abi: ContractAbi,
// bytecodeString: string,
// contractName: string,
// }
} catch (error) {
console.error('Compilation failed', error);
}

// get the accounts provided by your Ethereum node (like Ganache).
const accounts = await web3.eth.getAccounts();
const fromAddress = accounts[0];

// Set up the constructor parameter
const initial_message = '<initial_message>';
const deployParams = { arguments: [initial_message], from: fromAddress };

// Deploy the contract
let contractInstance;
try {
const deployFunction = contract.deploy(deployParams);
const gas = await deployFunction.estimateGas();
contractInstance = await deployFunction.send({from: fromAddress, gas});
console.log(
'\nThe contract has been deployed to address:\n ',
contractInstance.options.address,
);
} catch (error) {
console.error('Deployment failed', error);
}

// Read the current value of the message
let currentMessage;
try {
currentMessage = await contractInstance.methods.message().call();
console.log('Initial message:', currentMessage);
} catch (error) {
console.error('Failed to fetch message:', error);
}

// Update the message
const newMessage = 'Hello, Ethereum!';
const updateParams = { from: fromAddress };
try {
await contractInstance.methods.updateMessage(newMessage).send(updateParams);
console.log('Message updated successfully!');
} catch (error) {
console.error('Failed to update message:', error);
}

// Verify the message has been updated
try {
currentMessage = await contractInstance.methods.message().call();
console.log('Updated message:', currentMessage);
} catch (error) {
console.error('Failed to fetch updated message:', error);
}

And when running it with

node index.js

You should see something similar to:

Compilation was successful!
{
"abi": [
{
"inputs": [
{
"internalType": "string",
"name": "initMessage",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "message",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "newMessage",
"type": "string"
}
],
"name": "updateMessage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecodeString": "608060405234801561001057600080fd5b506040516107283803806107288339818101604052810190610032919061015a565b806000908051906020019061004892919061004f565b50506102bf565b82805461005b9061022f565b90600052602060002090601f01602090048101928261007d57600085556100c4565b82601f1061009657805160ff19168380011785556100c4565b828001600101855582156100c4579182015b828111156100c35782518255916020019190600101906100a8565b5b5090506100d191906100d5565b5090565b5b808211156100ee5760008160009055506001016100d6565b5090565b6000610105610100846101cc565b61019b565b90508281526020810184848401111561011d57600080fd5b6101288482856101fc565b509392505050565b600082601f83011261014157600080fd5b81516101518482602086016100f2565b91505092915050565b60006020828403121561016c57600080fd5b600082015167ffffffffffffffff81111561018657600080fd5b61019284828501610130565b91505092915050565b6000604051905081810181811067ffffffffffffffff821117156101c2576101c1610290565b5b8060405250919050565b600067ffffffffffffffff8211156101e7576101e6610290565b5b601f19601f8301169050602081019050919050565b60005b8381101561021a5780820151818401526020810190506101ff565b83811115610229576000848401525b50505050565b6000600282049050600182168061024757607f821691505b6020821081141561025b5761025a610261565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61045a806102ce6000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80631923be241461003b578063e21f37ce14610057575b600080fd5b61005560048036038101906100509190610228565b610075565b005b61005f61008f565b60405161006c91906102a2565b60405180910390f35b806000908051906020019061008b92919061011d565b5050565b6000805461009c90610383565b80601f01602080910402602001604051908101604052809291908181526020018280546100c890610383565b80156101155780601f106100ea57610100808354040283529160200191610115565b820191906000526020600020905b8154815290600101906020018083116100f857829003601f168201915b505050505081565b82805461012990610383565b90600052602060002090601f01602090048101928261014b5760008555610192565b82601f1061016457805160ff1916838001178555610192565b82800160010185558215610192579182015b82811115610191578251825591602001919060010190610176565b5b50905061019f91906101a3565b5090565b5b808211156101bc5760008160009055506001016101a4565b5090565b60006101d36101ce846102f5565b6102c4565b9050828152602081018484840111156101eb57600080fd5b6101f6848285610341565b509392505050565b600082601f83011261020f57600080fd5b813561021f8482602086016101c0565b91505092915050565b60006020828403121561023a57600080fd5b600082013567ffffffffffffffff81111561025457600080fd5b610260848285016101fe565b91505092915050565b600061027482610325565b61027e8185610330565b935061028e818560208601610350565b61029781610413565b840191505092915050565b600060208201905081810360008301526102bc8184610269565b905092915050565b6000604051905081810181811067ffffffffffffffff821117156102eb576102ea6103e4565b5b8060405250919050565b600067ffffffffffffffff8211156103105761030f6103e4565b5b601f19601f8301169050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b8381101561036e578082015181840152602081019050610353565b8381111561037d576000848401525b50505050565b6000600282049050600182168061039b57607f821691505b602082108114156103af576103ae6103b5565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fea264697066735822122082c59b20c4e3d2fe1910e039581a1734d393c358be11bda90acf0f4394bfa60c64736f6c63430008000033",
"contractName": "HelloWorld"
}

The contract has been deployed to address:
0x99e2E68EcDa59c7A836DA6127D1FEa6b1bfa8E1F
Initial message: <initial_message>
Message updated successfully!
Updated message: Hello, Ethereum!

Congratulations! You have successfully interacted with your Ethereum contract using web3-plugin-craftsman.

Conclusion and Next Steps

We have successfully set up web3 and web3-plugin-craftsman, created a HelloWorld contract, handled its compilation, deployed it onto the Ethereum network, and interacted with it.

This is a basic workflow of how you’d work with Ethereum contracts. However, Solidity is a powerful language and Ethereum is a flexible platform; there’s so much more to explore.

Possible next steps to deepen your understanding are:

  • Check the README file of web3-plugin-craftsman an explore it for things like the constructor options that let you for example pass multiple smart contracts files. And check the save the compilation result functionality...
  • Experiment with more complex smart contracts: Go beyond a “Hello World” example and create contracts for practical use cases, such as voting systems, token creation, decentralized exchanges, etc.
  • Understand Ethereum environments: Apart from local, understand the usage of the test nets (like Sepolia) and the Ethereum mainnet and learn to deploy your contract on them.
  • Check the official web3.js documentation and explore other tutorials like for example: The Solidity Events Guide I Wish I Had.
  • Learn about the Ethereum ecosystem: Get comfortable with various Ethereum development tools, libraries, wallets, and DApps.
  • Understand security concerns: Learn how to write secure contracts and understand potential vulnerabilities in contract creation.

Congrats on setting your foot in Ethereum development! Keep learning and exploring. Happy coding!

--

--