Using Ethereum Libraries to Deploy Smart Contracts on NFTMart
Introduction
This guide walks through using the Solidity compiler and two different Ethereum libraries to sign and send a transaction on NFTMart manually. The two libraries covered by this tutorial are:
Checking Prerequisites
The examples using both web3.js and ethers.js need you to install Node.js and NPM previously. For the web3.py, you need Python and PIP. As of the writing of this guide, the versions used were:
- Node.js v15.10.0
- NPM v7.5.3
Next, create a directory to store all of the relevant files:
mkdir incrementer && cd incrementer/
For the JavaScript libraries, first, you can create a simple package.json
file
(not required):
npm init --yes
In the directory, install the corresponding library and the Solidity compiler:
Web3.js
npm i web3 npm i solc@0.8.0
Ethers.js
npm i ethers npm i solc@0.8.0
The Contract File
The contract used is a simple incrementer, arbitrarily named Incrementer.sol. The Solidity code is the following:
pragma solidity ^0.8.0;
contract Incrementer {
uint256 public number;
constructor(uint256 _initialNumber) {
number = _initialNumber;
}
function increment(uint256 _value) public {
number = number + _value;
}
function reset() public {
number = 0;
}
}
The constructor
function, which runs when the contract is deployed, sets the
initial value of the number variable stored on-chain (default is 0). The
increment
function adds the _value
provided to the current number, but a
transaction needs to be sent, which modifies the stored data. Lastly, the
reset
function resets the stored value to zero.
Compiling the Contract
The only purpose of the compile file is to use the Solidity compiler to output
the bytecode and interface (ABI) our contract. (they were arbitrarily named
compile.js
)
The compile file for both JavaScript libraries is the same as they share the JavaScript bindings for the Solidity compiler (same package).
Web3.js/Ethers.js
//filename: compile.js
const fs = require('fs');
const solc = require('solc');
// Get Path and Load Contract
const source = fs.readFileSync('Incrementer.sol', 'utf8');
// Compile Contract
const input = {
language: 'Solidity',
sources: {
'Incrementer.sol': {
content: source,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
};
const tempFile = JSON.parse(solc.compile(JSON.stringify(input)));
const contractFile = tempFile.contracts['Incrementer.sol']['Incrementer'];
// Export Contract Data
module.exports = contractFile;
In the first part of the script, the contract's path is fetched, and its content read.
Next, the Solidity compiler's input object is built, and it is passed as input
to the solc.compile
function.
Lastly, extract the data of the Incrementer
contract of the Incrementer.sol
file, and export it so that the deployment script can use it.
Deploying the Contract
Regardless of the library, the strategy to deploy the compiled smart contract is somewhat similar. A contract instance is created using its interface (ABI) and bytecode. From this instance, a deployment function is used to send a signed transaction that deploys the contract.
For simplicity, the deploy file is composed of two sections. In the first section ("Define Provider & Variables"), the library to use and the ABI and bytecode of the contract are imported. Also, the provider and account from (with the private key) are defined.
The second section ("Deploy Contract") outlines the actual contract deployment
part. Note that for this example, the initial value of the number
variable was
set to 5. Some of the key takeaways are discussed next.
Web3.js
//filename: deploy.js
const Web3 = require('web3');
const contractFile = require('./compile');
/*
-- Define Provider & Variables --
*/
// Provider
const providerRPC = {
development: 'http://localhost:8545',
};
const web3 = new Web3(providerRPC.development); //Change to correct network
// Variables
const account_from = {
privateKey: 'YOUR-PRIVATE-KEY-HERE',
address: 'PUBLIC-ADDRESS-OF-PK-HERE',
};
const bytecode = contractFile.evm.bytecode.object;
const abi = contractFile.abi;
/*
-- Deploy Contract --
*/
const deploy = async () => {
console.log(`Attempting to deploy from account ${account_from.address}`);
// Create Contract Instance
const incrementer = new web3.eth.Contract(abi);
// Create Constructor Tx
const incrementerTx = incrementer.deploy({
data: bytecode,
arguments: [5],
});
// Sign Transacation and Send
const createTransaction = await web3.eth.accounts.signTransaction(
{
data: incrementerTx.encodeABI(),
gas: await incrementerTx.estimateGas(),
},
account_from.privateKey
);
// Send Tx and Wait for Receipt
const createReceipt = await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction
);
console.log(
`Contract deployed at address: ${createReceipt.contractAddress}`
);
};
deploy();
Ethers.js
//filename: deploy.js
const ethers = require('ethers');
const contractFile = require('./compile');
/*
-- Define Provider & Variables --
*/
// Provider
const providerRPC = {
development: {
name: 'minix-development',
rpc: 'http://localhost:8545',
chainId: 1500,
},
};
const provider = new ethers.providers.StaticJsonRpcProvider(
providerRPC.development.rpc,
{
chainId: providerRPC.development.chainId,
name: providerRPC.development.name,
}
); //Change to correct network
// Variables
const account_from = {
privateKey: 'YOUR-PRIVATE-KEY-HERE',
};
const bytecode = contractFile.evm.bytecode.object;
const abi = contractFile.abi;
// Create Wallet
let wallet = new ethers.Wallet(account_from.privateKey, provider);
/*
-- Deploy Contract --
*/
// Create Contract Instance with Signer
const incrementer = new ethers.ContractFactory(abi, bytecode, wallet);
const deploy = async () => {
console.log(`Attempting to deploy from account: ${wallet.address}`);
// Send Tx (Initial Value set to 5) and Wait for Receipt
const contract = await incrementer.deploy([5]);
await contract.deployed();
console.log(`Contract deployed at address: ${contract.address}`);
};
deploy();
Web3.js
In the first part of the script, the web3
instance (or provider) is created
using the Web3
constructor with the provider RPC. By changing the provider RPC
given to the constructor, you can choose which network you want to send the
transaction to.
The private key, and the public address associated with it, are defined for signing the transaction and logging purposes. Only the private key is required. Also, the contract's bytecode and interface (ABI) are fetched from the compile's export.
In the second section, a contract instance is created by providing the ABI.
Next, the deploy
function is used, which needs the bytecode and arguments of
the constructor function. This will generate the constructor transaction object.
Afterwards, the constructor transaction can be signed using the
web3.eth.accounts.signTransaction()
method. The data field corresponds to the
bytecode, and the constructor input arguments are encoded together. Note that
the value of gas is obtained using estimateGas()
option inside the constructor
transaction.
Lastly, the signed transaction is sent, and the contract's address is displayed in the terminal.
Ethers.js
In the first part of the script, different networks can be specified with a
name, RPC URL (required), and chain ID. The provider (similar to the web3
instance) is created with the ethers.providers.StaticJsonRpcProvider
method.
An alternative is to use the ethers.providers.JsonRpcProvide(providerRPC)
method, which only requires the provider RPC endpoint address. But this might
created compatibility issues with individual project specifications.
The private key is defined to create a wallet instance, which also requires the provider from the previous step. The wallet instance is used to sign transactions. Also, the contract's bytecode and interface (ABI) are fetched from the compile's export.
In the second section, a contract instance is created with
ethers.ContractFactory()
, providing the ABI, bytecode, and wallet. Thus, the
contract instance already has a signer. Next, the deploy
function is used,
which needs the constructor input arguments. This will send the transaction for
contract deployment. To wait for a transaction receipt you can use the
deployed()
method of the contract deployment transaction.
Lastly, the contract's address is displayed in the terminal.
Reading from the Contract (Call Methods)
Call methods are the type of interaction that don't modify the contract's storage (change variables), meaning no transaction needs to be sent.
For simplicity, the get file is composed of two sections. In the first section ("Define Provider & Variables"), the library to use and the ABI of the contract are imported. Also, the provider and the contract's address are defined.
The second section ("Call Function") outlines the actual call to the contract. Regardless of the library, a contract instance is created (linked to the contract's address), from which the call method is queried. Some of the key takeaways are discussed next.
Web3.js
//filename: get.js
const Web3 = require('web3');
const { abi } = require('./compile');
/*
-- Define Provider & Variables --
*/
// Provider
const providerRPC = {
development: 'http://localhost:8545',
};
const web3 = new Web3(providerRPC.development); //Change to correct network
// Variables
const contractAddress = 'CONTRACT-ADDRESS-HERE';
/*
-- Call Function --
*/
// Create Contract Instance
const incrementer = new web3.eth.Contract(abi, contractAddress);
const get = async () => {
console.log(`Making a call to contract at address: ${contractAddress}`);
// Call Contract
const data = await incrementer.methods.number().call();
console.log(`The current number stored is: ${data}`);
};
get();
Ethers.js
//filename: get.js
const ethers = require('ethers');
const { abi } = require('./compile');
/*
-- Define Provider & Variables --
*/
// Provider
const providerRPC = {
development: {
name: 'minix-development',
rpc: 'http://localhost:8545',
chainId: 1500,
},
};
const provider = new ethers.providers.StaticJsonRpcProvider(
providerRPC.development.rpc,
{
chainId: providerRPC.development.chainId,
name: providerRPC.development.name,
}
); //Change to correct network
// Variables
const contractAddress = 'CONTRACT-ADDRESS-HERE';
/*
-- Call Function --
*/
// Create Contract Instance
const incrementer = new ethers.Contract(contractAddress, abi, provider);
const get = async () => {
console.log(`Making a call to contract at address: ${contractAddress}`);
// Call Contract
const data = await incrementer.number();
console.log(`The current number stored is: ${data}`);
};
get();
Web3.js
In the first part of the script, the web3
instance (or provider) is created
using the Web3
constructor with the provider RPC. By changing the provider RPC
given to the constructor, you can choose which network you want to send the
transaction to.
The contract's interface (ABI) and address are needed as well to interact with it.
In the second section, a contract instance is created with web3.eth.Contract()
by providing the ABI and address. Next, the method to call can be queried with
the contract.methods.methodName(_input).call()
function, replacing contract
,
methodName
and _input
with the contract instance, function to call, and
input of the function (if necessary). This promise, when resolved, will return
the value requested.
Lastly, the value is displayed in the terminal.
Ethers.js
In the first part of the script, different networks can be specified with a
name, RPC URL (required), and chain ID. The provider (similar to the web3
instance) is created with the ethers.providers.StaticJsonRpcProvider
method.
An alternative is to use the ethers.providers.JsonRpcProvide(providerRPC)
method, which only requires the provider RPC endpoint address. But this might
created compatibility issues with individual project specifications.
The contract's interface (ABI) and address are needed as well to interact with it.
In the second section, a contract instance is created with ethers.Contract()
,
providing its address, ABI, and the provider. Next, the method to call can be
queried with the contract.methodName(_input)
function, replacing contract
methodName
, and _input
with the contract instance, function to call, and
input of the function (if necessary). This promise, when resolved, will return
the value requested.
Lastly, the value is displayed in the terminal.
Interacting with the Contract (Send Methods)
Send methods are the type of interaction that modify the contract's storage (change variables), meaning a transaction needs to be signed and sent.
For simplicity, the increment file is composed of two sections. In the first
section ("Define Provider & Variables"), the library to use and the ABI of the
contract are imported. The provider, the contract's address, and the value of
the increment
function are also defined.
The second section ("Send Function") outlines the actual function to be called with the transaction. Regardless of the library, a contract instance is created (linked to the contract's address), from which the function to be used is queried.
Web3.js
//filename: increment.js
const Web3 = require('web3');
const { abi } = require('./compile');
/*
-- Define Provider & Variables --
*/
// Provider
const providerRPC = {
development: 'http://localhost:8545',
};
const web3 = new Web3(providerRPC.development); //Change to correct network
// Variables
const account_from = {
privateKey: 'YOUR-PRIVATE-KEY-HERE',
};
const contractAddress = 'CONTRACT-ADDRESS-HERE';
const _value = 3;
/*
-- Send Function --
*/
// Create Contract Instance
const incrementer = new web3.eth.Contract(abi, contractAddress);
// Build Increment Tx
const incrementTx = incrementer.methods.increment(_value);
const increment = async () => {
console.log(
`Calling the increment by ${_value} function in contract at address: ${contractAddress}`
);
// Sign Tx with PK
const createTransaction = await web3.eth.accounts.signTransaction(
{
to: contractAddress,
data: incrementTx.encodeABI(),
gas: await incrementTx.estimateGas(),
},
account_from.privateKey
);
// Send Tx and Wait for Receipt
const createReceipt = await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction
);
console.log(`Tx successful with hash: ${createReceipt.transactionHash}`);
};
increment();
Ethers.js
//filename: increment.js
const ethers = require('ethers');
const { abi } = require('./compile');
/*
-- Define Provider & Variables --
*/
// Provider
const providerRPC = {
development: {
name: 'minix-development',
rpc: 'http://localhost:8545',
chainId: 1500,
},
};
const provider = new ethers.providers.StaticJsonRpcProvider(
providerRPC.development.rpc,
{
chainId: providerRPC.development.chainId,
name: providerRPC.development.name,
}
); //Change to correct network
// Variables
const account_from = {
privateKey: 'YOUR-PRIVATE-KEY-HERE',
};
const contractAddress = 'CONTRACT-ADDRESS-HERE';
const _value = 3;
// Create Wallet
let wallet = new ethers.Wallet(account_from.privateKey, provider);
/*
-- Send Function --
*/
// Create Contract Instance with Signer
const incrementer = new ethers.Contract(contractAddress, abi, wallet);
const increment = async () => {
console.log(
`Calling the increment by ${_value} function in contract at address: ${contractAddress}`
);
// Sign-Send Tx and Wait for Receipt
const createReceipt = await incrementer.increment([_value]);
await createReceipt.wait();
console.log(`Tx successful with hash: ${createReceipt.hash}`);
};
increment();
Web3.js
In the first part of the script, the web3
instance (or provider) is created
using the Web3
constructor with the provider RPC. By changing the provider RPC
given to the constructor, you can choose which network you want to send the
transaction to.
The private key, and the public address associated with it, are defined for signing the transaction and logging purposes. Only the private key is required. Also, the contract's interface (ABI) and address are needed to interact with it. If necessary, you can define any variable required as input to the function you are going to interact with.
In the second section, a contract instance is created with web3.eth.Contract()
by providing the ABI and address. Next, you can build the transaction object
with the contract.methods.methodName(_input)
function, replacing contract
,
methodName
and _input
with the contract instance, function to call, and
input of the function (if necessary).
Afterwards, the transaction can be signed using the
web3.eth.accounts.signTransaction()
method. The data field corresponds to the
transaction object from the previous step. Note that the value of gas is
obtained using estimateGas()
option inside the transaction object.
Lastly, the signed transaction is sent, and the transaction hash is displayed in the terminal.
Ethers.js
In the first part of the script, different networks can be specified with a
name, RPC URL (required), and chain ID. The provider (similar to the web3
instance) is created with the ethers.providers.StaticJsonRpcProvider
method.
An alternative is to use the ethers.providers.JsonRpcProvide(providerRPC)
method, which only requires the provider RPC endpoint address. But this might
created compatibility issues with individual project specifications.
The private key is defined to create a wallet instance, which also requires the provider from the previous step. The wallet instance is used to sign transactions. Also, the contract's interface (ABI) and address are needed to interact with it. If necessary, you can define any variable required as input to the function you are going to interact with.
In the second section, a contract instance is created with ethers.Contract()
,
providing its address, ABI, and wallet. Thus, the contract instance already has
a signer. Next, transaction corresponding to a specific function can be send
with the contract.methodName(_input)
function, replacing contract
,
methodName
and _input
with the contract instance, function to call, and
input of the function (if necessary). To wait for a transaction receipt, you can
use the wait()
method of the contract deployment transaction.
Lastly, the transaction hash is displayed in the terminal.
Running the Scripts
For this section, the code shown before was adapted to target a development node, which you can run by following Setting Up a Local NFTMart Node . Also, each transaction was sent from the pre-funded account that comes with the node:
- Private key:
99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342
- Public address:
0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b
(1). First, deploy the contract by running (note that the directory was renamed for each library):
Web3.js
node deploy.js
Ethers.js
node deploy.js
This will deploy the contract and return the address:
Web3.js
/web3js_incrementer$ node deploy.js
Attempting to deploy from account 0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b
Contract deployed at address: 0xC2Bf5F29a4384b1aB0C063e1c666f02121B6084a
Ethers.js
/etherjs_incrementer$ node deploy.js
Attempting to deploy from account: 0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b
Contract deployed at address: 0x42e2EE7Ba8975c473157634Ac2AF4098190fc741
(2). Next, set the contract address in the get.js and increment.js , then , run the increment file. You can use the get file to verify the value of the number stored in the contract before and after increment it:
Web3.js
# Get value
node get.js
# Increment value
node increment.js
# Get value
node get.js
Ethers.js
# Get value
node get.js
# Increment value
node increment.js
# Get value
node get.js
This will display the value before the increment transaction, the hash of the transaction, and the value after the increment transaction:
Web3.js
/web3js_incrementer$ node get.js
Making a call to contract at address: 0xC2Bf5F29a4384b1aB0C063e1c666f02121B6084a
The current number stored is: 5
/web3js_incrementer$ node increment.js
Calling the increment by 3 function in contract at address: 0xC2Bf5F29a4384b1aB0C063e1c666f02121B6084a
Tx successful with hash: 0x837a599fd07ab83d11a2d2ee097f9afc82dcce491d2acf33eb32546b15a23fd3
/web3js_incrementer$ node get.js
Making a call to contract at address: 0xC2Bf5F29a4384b1aB0C063e1c666f02121B6084a
The current number stored is: 8
Ethers.js
/etherjs_incrementer$ node get.js
Making a call to contract at address: 0x42e2EE7Ba8975c473157634Ac2AF4098190fc741
The current number stored is: 5
/etherjs_incrementer$ node increment.js
Calling the increment by 3 function in contract at address: 0x42e2EE7Ba8975c473157634Ac2AF4098190fc741
Tx successful with hash: 0xcce5908307dc0d2e29b3dda1503cc2c4b3ea710db7d520567c5b81ea516a4136
/etherjs_incrementer$ node get.js
Making a call to contract at address: 0x42e2EE7Ba8975c473157634Ac2AF4098190fc741
The current number stored is: 8