Experimenting Tezos EVM Rollups with ethers.js and web3.js libraries

Luiz Milfont
6 min readJul 20, 2023

Sharing my first attempts results with Tezos community

EVM Rollups are coming to Tezos. In my opinion it might be a real game changer, helping to lower the entry barrier for blockchain developers to join the Tezos network.

In the last couple of days I have been experimenting with this. My main goal is to be able to interact with Tezos EVM Rollup from the most common NodeJS / Javascript libraries used by the Ethereum blockchain developers to deploy smart contracts and call its methods.

For this article I will be using the TACO ERC-20 token initiative, so having acquired some TACOs is a requirement. I will be using Tezos Ghostnet test network and Nomadic Lab’s EVM Rollup, which public JSON-RPC endpoint lies at https://evm.ghostnet-evm.tzalpha.net

Please follow Nomadic Lab’s article as a requirement to continue.

The idea here is to communicate programatically with Tezos EVM rollup “from the Ethereum world”. There are plenty of options regarding programming language libraries (ethers.js, web3.js, Hardhat, Remix IDE, for example). I will show two (tested) working samples. The first one uses ethersJS library to interact with TACO token from NodeJS. The second one uses web3.js

Unlock the power of SEO dominance and propel your website to the top of Google searches with our game-changing tool! 🚀 #FirstPageSuccess

The Javascript code below imports EthersJS library, sets Tezos Ghostnet EVM Rollup as its JSON-RPC provider, then instantiate an entry point for calling the contract’s methods. It will read some of the contract’s info (as symbol, number of decimals, user’s TACO balance) and output it on screen.

For it to properly work, please insert your wallet private key and replace the fromAddress and toAddress of the transfer operation with accounts that you own.

// Import the EthersJS library.
const { ethers, Wallet, utils } = require("ethers");

const callContract = async function ()
{
try
{
// Define Tezos Ghostnet EVM rollup as provider.
const url = 'https://evm.ghostnet-evm.tzalpha.net';
const provider = new ethers.providers.JsonRpcProvider(url);

// In Ethereum you have to previously know the contract's entrypoints.
const abi = [
// Read-Only Functions
"function balanceOf(address owner) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",

// Authenticated Functions
"function transfer(address to, uint amount) returns (bool)",

// Events
"event Transfer(address indexed from, address indexed to, uint amount)"
];

// My wallet accounts. Will transfer TACO ERC-20 token from one to another.
const fromAddress = '0xeD2956e002FD35a069eCDB9e9b870648f11f202a';
const toAddress = '0x21C2bc9374F5A1739504d2315b489F1ac2eb350a';

// TACO token contract address.
const tokenContract = "0x4A0225335fBBE8dC67F4487992df5d966a932575";

// Signer. This will be used to... sign operations. Insert your own private key here.
const signer = new ethers.Wallet('0x[yourWalletPrivateKey]', provider);

// "erc20" variable will be our contract instance.
const erc20 = new ethers.Contract(tokenContract, abi, provider);

// Here we get information about our token, from the blockchain.
const symbol = await erc20.symbol();
const decimals = await erc20.decimals();
const balanceOf = await erc20.balanceOf(signer.getAddress());
const myBalance = balanceOf / (10 ** decimals);

// Output to screen...
console.log('Symbol : ' + symbol);
console.log('Decimals : ' + decimals);
console.log('Balance : ' + myBalance);


// Now lets send 01 TACO token.
console.log('\nTransfering 1 TACO token...');

// Define and parse token amount.
const amount = ethers.utils.parseUnits("1.0", decimals);

// Define the data parameter (will be used inside the transaction object).
const data = erc20.interface.encodeFunctionData("transfer", [toAddress, amount] )

// Create and send the transaction object.
const tx = await signer.sendTransaction({
to: tokenContract, // address of TACO contract.
from: signer.address, // address of sender.
value: ethers.utils.parseUnits("0.000", "ether"),
data: data
});


// Waiting for the transaction to be completed...
const receipt = await tx.wait();

console.log(receipt);

}
catch (error)
{
console.log("Error when calling contract.");
console.log(error);
}
};

callContract();

Notice that (at the time of this writing) the code will return an error in the “transfer” operation. But the transfer works, as seen in the image below.

Unlock the power of SEO dominance and propel your website to the top of Google searches with our game-changing tool! 🚀 #FirstPageSuccess

Error: Transaction hash mismatch from Provider.sendTransaction. (expectedHash=”0xa6a81d77919ad5afe0503aeb7a8aefb6aab9065297b2d1b33099ae5c82e70ac4",returnedHash=”0x6691ec82c982b056b14c6f4aec4ad79a4cc074eaf24ff96fcd2ead197a5d8c41", code=UNKNOWN_ERROR, version=providers/5.7.2)

Web3.js

Now lets see the very same example, but using another Javascript library (web3.js):

// Import the Web3 library.
const { Web3 } = require("web3");

const callContract = async function ()
{
try
{
// Define Tezos Ghostnet EVM rollup as provider.
const url = 'https://evm.ghostnet-evm.tzalpha.net';
var web3 = new Web3(url);

// In Ethereum you have to previously know the contract's entrypoints.
const abi = [
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"type": "function"
},
// transfer
{
'constant': false,
'inputs': [
{
'name': '_to',
'type': 'address'
},
{
'name': '_value',
'type': 'uint256'
}
],
'name': 'transfer',
'outputs': [
{
'name': '',
'type': 'bool'
}
],
'type': 'function'
}
];

// My wallet accounts. Will transfer TACO ERC-20 token from one to another.
const fromAddress = '0xeD2956e002FD35a069eCDB9e9b870648f11f202a';
const toAddress = '0x21C2bc9374F5A1739504d2315b489F1ac2eb350a';

// TACO token contract address.
const tokenContract = "0x4A0225335fBBE8dC67F4487992df5d966a932575";

// Secret key used to sign operations. Insert your own private key here.
const sk = '0x[yourWalletPrivateKey]';

// "erc20" variable will be our contract instance.
const erc20 = new web3.eth.Contract(abi, tokenContract, { from: fromAddress } );

// Here we get information about our token, from the blockchain.
const symbol = await erc20.methods.symbol().call();
const decimals = await erc20.methods.decimals().call();
const name = await erc20.methods.name().call();
const balanceOf = await erc20.methods.balanceOf(fromAddress).call();
const myBalance = parseInt(balanceOf.toString()) / Math.pow(10, parseInt(decimals.toString()));

// Output to screen...
console.log('Symbol : ' + symbol);
console.log('Decimals : ' + decimals);
console.log('Name : ' + name);
console.log('Balance : ' + myBalance);

// Now lets send 01 TACO token.
console.log('\nTransfering 1 TACO token...');

// Define and parse token amount.
let amount = 1;
let tokenAmount = web3.utils.toWei(amount.toString(), 'ether');

// Define the data parameter (will be used inside the transaction object).
let data = erc20.methods.transfer(toAddress, tokenAmount).encodeABI()

// Create transaction object.
let tx = {
gas: 22000,
value: "0x00",
data: data,
from: fromAddress,
to: tokenContract,
gasPrice: await web3.eth.getGasPrice(),
gasLimit: 3000000
}

// Sign the transaction, then send.
await web3.eth.accounts.signTransaction(tx, sk)
.then((signedTx) => {
console.log(signedTx);
return web3.eth.sendSignedTransaction(signedTx.rawTransaction)
.then((res) => { console.log(res); })
.catch((err) => { throw err; })
})
.catch((error) => { throw error } );

}
catch (error)
{
console.log("Error when calling contract.");
console.log(error);
}
};

callContract();

Apparently, web3.js works better for now, without any error messages. The code execution will show some hashes. TACO token is correctly transfered between accounts and transaction confirmation can be seen on block explorer:

Conclusion

Although it is still too early to work with this technology (that is at Alpha stage of development), it is a great initiative that I really consider a game changer for Tezos.

I decided to write this article in order to inspire others to try this. There are many NodeJS / Javascript developers out there. The embedded code in this article is so easy to understand and might represent a starting point for whoever is willing to experiment with Tezos EVM Rollups right now!

--

--