Transitioning from Web 2 to Web 3: Guide for Frontend Engineers

Francisco Ramos
21 min readNov 6, 2023
Source: The Shift From Web2 to Web3

I got into Crypto probably in the same way many others did: 2017 peak of bull market, let’s make some money!!. As sad as it sounds, this is how most of us learnt about Crypto, FOMO’ing. But my curiosity, above all when it comes to technologies, pushed me to try to understand what’s underneath all that Crypto stuff: Blockchain. For those who are new to this technology, and the whole Crypto space, there is a very deep rabbit hole here. I haven’t yet reached the bottom, and I have the feeling that I’m still scratching the surface.

Why Frontend?

I’ve been doing Web Development for quite sometime now. To give you an idea, I started building for IE 5.5 😅. I touched different areas within Web Development, from Backend to DevOps, QA, and even DB admin… but my strengths really lie in Frontend topics as I spend more time building for the Browser. I think Frontend is where the real fun is. Here are my reasons:

  • Visual impact. It’s very rewarding to see your work in the Browser.
  • User Experience. You have a direct impact in the experience of the user.
  • Creativity. The art of translating design, sometimes mock-ups, into functional UI, not only requiring logical coding but also a creative touch.
  • Constant evolution. Frontend is constantly evolving, new frameworks, tools, architectures, best practices… It’s a non-stop learning. Might be tiring, but definitely not boring.
  • Problem-solving. I honestly haven’t experienced other areas that present so many different puzzles to solve.
  • Versatility. The skills acquired in Frontend can be applied to other domains. I mean, Javascript is everywhere.

Why would I want to transition to Web3?

There is no need to spend much time explaining what Web3 is, as a simple googling will give you enough to get you started with the definition. It’s still an evolving concept though, so you might find slightly different definitions, but the main technology behind Web3 is Blockchain. There are different types of Blockchains which could be more or less decentralized, permissioned or permissionless ones, but the core ideas of Web3 are:

  • Decentralization: no one really owns it.
  • Permissionless: everyone has equal access and participation.
  • Trustless: does not rely on third parties.

Now, I don’t know what you think, but I do love these ideas. It feels like this is how Internet should be. Web2, which is the type of web applications we currently are building, has none of these great ideas at its core, and this is the reason why you might want to transition to Web3.

As a Frontend Engineer, you already know how to build and deploy apps with beautiful and engaging UI, using all your favourite tools to do so. Good news!!, you are already half way to transition 😃. But the communication with backend is slightly different. There are some other aspects that you might not be familiar with, such as RPC nodes, wallets, transaction receipt, contract’s ABI, dealing with big numbers, etc.

Ethereum and EVM compatible blockchains

Before I continue, I’d like to clarify that in this article I’m gonna focus on Ethereum and EVM-compatible chains. Ethereum is a decentralized Blockchain with three key components:

  • Ether: this is the native coin that incentivises people to maintain the network, and fuel the execution of the next component.
  • Smart Contracts: these are just code that executes in the Blockchain. They might have state that can change when running this code.
  • DApps: these are applications built on top of Ethereum, and therefore they are decentralized — hence the “D”. Here we have Frontend UI interacting with the Blockchain and Smart Contracts.

So, these dApps are the ones we want to build the Frontend for, and this interaction with the Blockchain is what makes them different from web2 apps.

Regarding EVM compatibility, these are Blockchain platforms that have implemented an environment which can interpret and execute Smart Contracts written for the Ethereum network. This means that developers can deploy the same Smart Contract code they wrote for Ethereum onto these EVM-compatible chains with minimal or no changes. Some of the most popular ones are: Binance Smart Chain, Polygon or Avalanche

How do I communicate with this new Backend?

As a Web2 Frontend developer, you are used to making a request to the Backend, which might fetch some data from DB, run some logic and send back data to the Frontend. You get that response, typically in JSON format, and render the UI according to the new data. You use APIs such as the Browser built-in Fetch, or maybe some external library such as Axios. Whether this request is reading from or writing in DB, the only thing that might change is the method of that request — if we are using Rest, and we follow conventions. This last point is important since in Web3 there is a bigger difference depending on whether we are reading the Blockchain’s current state or updating its state.

In Web3 Frontend development, this is a bit different. Remember, the Blockchain is distributed across multiple computers or nodes. Each node has a copy of the of Blockchain, and if you want to, let’s say, read something, you need to query one of these nodes. How do you access to them? they provide you with an endpoint, same as in Web2. This endpoint is called RPC (Remote Procedure Call) URL, and these nodes are RPC nodes. Unfortunately you might have to pay if you want to access one of them. I mean, you can have free access for development purposes, but they have a limitation in terms of how many queries you can do. You may be wondering: can I run a node myself so I don’t need to pay any fees? Yes you can!!, but believe me, it’s not that simple. At the end, most likely it won’t be of your concern, since it’ll be paid by the company 😉.

Ok, so there is an endpoint I can query. Can I use Fetch API for this?, yes, you can, but you wouldn’t normally use plain Javascript to do so. I’ll explain in the next section.

Interacting with the Blockchain

These nodes implement what’s called JSON-RPC protocol. As a Frontend dev, what you need to know here is the format of that JSON you are sending to the node as the body of the request. Example:

const response = await fetch('https://rpc-url', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
// Protocol version
jsonrpc: '2.0',

// [Optional] Used to match the response with the request that's
// replying to.
id: 'unique_id',

// Method you want to invoke in the API
method: 'method_name',

// [Optional] Method's arguments. The order is important
params: [arg1, arg2, ...],
}),
})

const data = await response.json()
console.log(data)
/**
* {
* jsonrpc: '2.0',
* id: 'unique_id',
* result: ...
* }
*/

Following you have a list of methods supported by Ethereum nodes, and more details about the API, if you are curious: Ethereum JSON-RPC Specification and JSON-RPC API

As I mentioned before, today no one really makes these requests sending JSON bodies like this. Also, you need to manually process the response and get what you are looking for out of it. Instead we have great libraries out there that give us nice abstractions around these methods. Most well-known ones are: Web3.js (v4.x at the time of writing), Ethers.js (v6.x at the time of writing), Viem (v1.x at the time of writing). Personally my favourite one is Viem, which in combination with Wagmi, are probably the best libraries for Web3. Let’s compared how you would get the balance in plain Javascript and using one of these libraries:

const account = '0xc0ffee254729296a45a3885639AC7E10F9d54979'
const block = 'latest'

// Using plain Javascript
const response = await fetch('https://rpc-url', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_getBalance',
params: [account, block],
}),
})

const data = await response.json()
const balance = BigInt(data.result)

/* ==================================== */

// Using a web3 lib
const provider = new web3lib.JsonRpcProvider('https://rpc-url')
const balance = await provider.getBalance(account, block)

One might argue that the benefits of this abstraction is not that big, and you could simply implement that abstraction yourself around the methods you are gonna be using in your app, so we don’t bloat our bundle with more packages. Well, it’s not that simple, this JSON body could get very complex, above all when you are trying to interact with a Smart Contracts, and send signed transactions — we’ll get to this part later — . Just trust me here, you want to use one of these web3 libraries 😉.

Interacting with Smart Contracts

I don’t want to get into details around Smart Contracts since this is more a guide for Frontend Engineers, but as part of your job you must definitely understand and know how to write, compile and deploy them, but there is plenty of information out there for you to explore the topic. The most common language to write Smart Contracts is Solidity. As a Frontend dev, you’ve already mastered Javascript(/Typescript). Learning Solidity should not be a problem as it looks sort of like Typescript: statically-typed curly-braces language. It was actually influenced by multiple existing programming languages such as C++, Python… and JavaScript 😃.

I mentioned earlier on that a Smart Contract is nothing more than code that runs in the blockchain. It’s got methods, publicly exposed or internal ones. It can have state, and this last part is important since depending on whether or not this state changes, Frontend will behave differently than in Web2 when you change state in the backend. This is an example of a very simple Smart Contract:

pragma solidity ^0.8.20;

contract Counter {
uint public count;

function get() public view returns (uint) {
return count;
}

function inc() public {
count += 1;
}

function dec() public {
count -= 1;
}
}

How can we call these methods and/or read the state?. Well, let’s first use low level logic before using a web3 library. Trust me, things will then look way much easier. Do you remember these RPC nodes we talked about earlier on, and that they have a list of supported JSON API methods? there are two important methods to interact with Smart Contracts:

  • eth_call: This is a read-only operation that executes a contract function on the local node without creating a transaction on the blockchain — we’ll get to this transaction thing next — . It can also be used to query the contract’s state and doesn’t modify anything on the blockchain. It returns the result of the function call directly as a response.
const txObject= {
// Contract address
to: '0x60463799Dad8bbF61084aea8DeF20071541cA73A',

// This could be a very long hex string, but here
// we just have the encoded name of the method `get()`
data: '0x6d4ce63c'
}

const response = await fetch('https://rpc-url', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'eth_call',
params: [txObject, 'latest'],
}),
});

const data = await response.json()
/**
* {
* jsonrpc: '2.0',
* id: 2,
* result: '0x0000…0003'
* }
*/
const count = Number(data.result) // 3
  • eth_sendTransaction: This is where things get a bit complicated compared to Web2, but bear with me. If the contract’s method you wanna call changes state — in our Counter contract example, inc() and dec() methods — , you need to send a transaction to the network, which will be mined into a block. This operation incurs gas fees and returns a transaction receipt, which includes information about the transaction’s status and gas used. I know, I know, what’s all these stuff: mining into blocks, gas fees, transaction receipts… 🤔
const txObject= {
// User's account signing the transaction
// and paying gas for this operation
from: "0xB3cAe61C9815cb40141a09330a35971fc453AdE4",

// Contract address
to: '0x60463799Dad8bbF61084aea8DeF20071541cA73A',

// Encoded name of the method `inc()`
data: '0x371303c0'
}

// window.ethereum => injected provider
// We are assuming the user's wallet is already connected
// TODO: explain all this 😱
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [txObject]
})

console.log(txHash)
// 0x3cbe3abdc5586f8d2172245679adc7b4344a9f7d98352b46622378713d9d200a

There is way more going on here, so let’s unpack as much as possible without going crazy.

Transactions and Receipts

There are three reasons why you might want to send a transaction:

  1. Value transfer. You want to transfer native coins, in this case Ether, from one wallet to another.
  2. Smart Contract creation. You want to deploy a Smart Contract.
  3. Smart Contract interaction. You want to call its methods… but remember, you only really need a transaction if there is change of state.

These transactions need a digital signature — we’ll get to this signature thing next— provided by the sender, which proves they have control over the account. Also, they cost gas, and the sender will have to pay that gas. This is the fuel that allows the network to operate, in the same way that a car needs gasoline to run. There is certain flexibility regarding how much gas they are willing the pay and at what price. This can be configured in that txObject seen in the previous example. There are some good articles out there explaining everything about gas.

You see, in Web2 we normally make an API call to backend in order to, let’s say, change some data in DB (create, update or delete something), and then we get back a response: success or failed. By the time we get that response, we already know for sure that the transaction either succeeded or failed. In Web3 we actually don’t know yet, even after getting a response from backend. What we get instead is a transaction hash, which we saw in the previous example, and that we stored into the variable txHash. This string uniquely identifies the transaction, and can be used to monitor it in a block explorer, or programmatically get the transaction receipt.

Wait, what’s now a transaction receipt? Right. Let’s first have a look at the typical transaction flow:

  1. User initiates a transaction from a dApp or wallet.
  2. User signs transaction with their wallet.
  3. Wallet sends the transaction to a node.
  4. Node verifies the transaction is valid and add it to a mempool. This is sort of like a waiting area.
  5. Since the node is connected to a group of peers, the transaction is broadcast into other nodes’ own mempool.
  6. Miners, which have a specific type of node, receive the transaction, validate it, and attempt to add it to a block.
  7. One miner successes in doing so, and adds a block with the transaction to the chain.
  8. The new block is broadcast over the network.
  9. Other nodes receive the new block with the transaction, and it’s removed from their mempool.

We can get the receipt the moment the transaction is finally mined and added to a block. Then we can query a node to get this receipt based on the transaction hash:

const txHash = '0x3cbe3abdc5586f8d2172245679adc7b4344a9f7d98352b46622378713d9d200a'

// Important note: this could take minutes
const response = await fetch('https://rpc-url', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 3,
method: 'eth_getTransactionReceipt',
params: [txHash],
}),
});

const data = await response.json()
// This is what we get:
{
jsonrpc: "2.0",
id: 3,
result: {
transactionHash: "0x3cbe3abdc5586f8d2172245679adc7b4344a9f7d98352b46622378713d9d200a",
blockHash: "0x67c5a3a84c9b8dce6609395628639650eb31c04a1cfd790c40894bc3c9a02ec6",
blockNumber: "0x980793",
logs: [],
contractAddress: null,
effectiveGasPrice: "0x59682f10",
cumulativeGasUsed: "0xbfdca",
from: "0xb3cae61c9815cb40141a09330a35971fc453ade4",
gasUsed: "0x6732",
logsBloom: "0x0000…0000",

// This field is important. Tells us whether
// the transaction succeded (0x1), or failed (0x0)
status: "0x1",

to: "0x60463799dad8bbf61084aea8def20071541ca73a",
transactionIndex: "0x8",
type: "0x2"
}
}

Something important to keep in mind here is that the previous RPC call to get the transaction receipt could take between a few seconds to minutes depending on several factors such as amount paid to process the transaction or how busy the network is at the time of processing it. Why is this important?, well, we might want to adopt a different strategy in our UI to indicate the user that there is a pending transaction. Think about it, showing some kind of loading indicator blocking part of the UI might not always be a good idea as it could take minutes before we get a response. The user of our dApp might want to keep using it and/or send more transactions while there is one, or even multiple ones, still pending.

Finally about wallets and signatures

This part is quite different from Web2. Remember when I talked about calling contract’s methods that change state, meaning, you’re gonna write stuff in the Blockchain? for this you need to send a transaction to the network, which costs gas and requires digital signature that proves account’s ownership. How can we sign those transactions and pay gas fees to get it processed? Well, this is where a wallet comes in.

Wallets are digital tools — there are other types, but let’s not complicate things — that store private and public keys. What are these keys?

  • The public key is derived from the private key and is used to create an address, which is what you share with others to receive funds.
  • The private key is known only to the owner of the wallet. It is used to sign transactions, effectively proving ownership of the funds and granting the permission to send those funds, as well as signing other type of transactions such as calling contract’s methods, etc.

That’s it, that’s what the wallet stores. Actually, the name “wallet” is kind of misleading, since it does not store your funds, just these keys.

So, the wallet is a very important piece when building dApps. Without it our application would have limited functionality. In a normal web2 app, the user would have a form to enter credentials to have access to more functionality and other private sections in our application. This means that this user had to previously go through a registration process, where they normally setup their passwords, and most likely give away some private detail such as name, phone number, email, etc. In web3 apps, the user only has to connect their wallets, which is what identifies them — their digital identities — , and that’s it, they are in. Think about all the infrastructure required for that registration and login process. In Web3, none of that is needed.

With that out of the way, let’s have a look again at this piece of code I previously showed:

const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [...]
})

What is that ethereum object in the global window? that’s been injected by a Browser extension, specifically a wallet extension. There are many Browser wallet extensions, but you need to be careful as the injected API might differ from wallet to wallet. The one in the example is an Ethereum wallet. One of the most well known Ethereum wallets is MetaMask. Fortunately there are great solutions out there that will help us to solve inconsistencies and other differences between wallets so you don’t need to handle this manually. They’re sort of like a single connect solution for multiple different wallets. They also come with a lot of functionality out of the box. Some of these great libraries are: Web3Modal, Web3-Onboard, RainbowKit, ConnectKit… and more if you google it.

Let’s compare how you would build a connect button using plain Javascript with an injected provider and then using one of solutions mentioned:

<!-- Plain Javascript -->

<script>
// 'disconnected' | 'connecting' | 'connected'
let connection = 'disconnected'

async function connectWallet() {
// First, check if we have access to window.ethereum
if (window.ethereum) {
connection = 'connecting'

try {
// Request account access if needed
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
})

connection = 'connected'

// Accounts now exposed, you can use them to
// sign transactions or read balances
console.log('Connected', accounts[0])
return accounts[0]
} catch (error) {
console.error('User denied account access', error);
connection = 'disconnected'
}
} else {
connection = 'disconnected'

// If window.ethereum is not found, then MetaMask
// is not installed or not enabled
alert('Please install MetaMask!')
}
}
</script>

<button disabled={connecting} on:click={connectWallet}>
{connection === 'disconnected'
? 'Connect your Wallet'
: connection === 'connecting'
? 'Connecting…'
: connection === 'connected'
? 'Connected'
: '¯\_(ツ)_/¯'
}
</button>

When you hit the part that requests account access, the wallet, in this case MetaMask, pops up asking to select the accounts you want to connect to the site:

MetaMask wallet
Connect with MetaMask

Let’s now have a look now how we would do the same using one of these solutions:

<!--
Renders a button which when clicked on
a lot of magic happens
-->
<w3m-button />

That’s it 😁… well, it’s not as simple as it looks, since it still requires some initial configuration, and also if you wanna style that button to follow your design. But the point here is that once that configuration is done, you get tons of functionality out of the box, and very little code to maintain, which means fewer bugs.

Now, imagine you have multiple wallets you could connect. These libraries are a great help in that regard. When you try to connect, you can chose the wallet you want to use:

Web3Modal
Connect Wallet

Not only can you connect to your Browser extension wallets, but also Mobile wallets by scanning QR codes:

WalletConnect QR Code

And once you are connected, you get more functionality for free, such as current balance, block explorer links, copy address, list of networks, disconnect, etc.

Last important point regarding wallets. Once you are connected, if the transaction requires signature or user’s approval, the wallet pops up automatically. For instance, as seen previously when we tried to increase the counter in our Counter contract example:

const txObject= {
from: "0xB3cAe61C9815cb40141a09330a35971fc453AdE4",
to: '0x60463799Dad8bbF61084aea8DeF20071541cA73A',
data: '0x371303c0' // encoded `inc()` method
}

const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [txObject]
})

Wallet opens with information about the transaction that’s about to be sent, such as how much it’ll cost, and other details. It will ask you to confirm (or reject). Once you confirm, the wallet takes care of signing the transaction using the private key, and all this is sent to the node.

MetaMask

As you can see, you don’t need to programmatically sign the transaction, which would be complex — I think, as I never did it manually — . The wallet is responsible of doing that.

Last piece of the puzzle: Contract’s ABI

ABI stands for Application Binary Interface. What is this for?, well, when you interact with a Smart Contract, you are calling methods, right?. These methods may have arguments with their corresponding types. The methods may return values, which also have some type. The question arises, how do you even call a method, passing the right arguments, of something that’s compiled and lives in the Blockchain? To give you an idea, this is what our Counter contract looks like — you can actually see it here deployed if still available by the time you’re reading this:

0x608060405234801561000f575f80fd5b506004361061004a575f3560e01
c806306661abd1461004e578063371303c01461006c5780636d4ce63c1461
0076578063b3bcfa8214610094575b5f80fd5b61005661009e565b6040516
1006391906100f7565b60405180910390f35b6100746100a3565b005b6100
7e6100bd565b60405161008b91906100f7565b60405180910390f35b61009
c6100c5565b005b5f5481565b60015f808282546100b4919061013d565b92
505081905550565b5f8054905090565b60015f808282546100d6919061017
0565b92505081905550565b5f819050919050565b6100f1816100df565b82
525050565b5f60208201905061010a5f8301846100e8565b92915050565b7
f4e487b710000000000000000000000000000000000000000000000000000
00005f52601160045260245ffd5b5f610147826100df565b9150610152836
100df565b925082820190508082111561016a57610169610110565b5b9291
5050565b5f61017a826100df565b9150610185836100df565b92508282039
0508181111561019d5761019c610110565b5b9291505056fea26469706673
582212206af71015421ffd0e8a451da6f779bcbf56a2ada381eaa8e0041e3
efe6fe0390964736f6c63430008160033

The ABI is essentially a user-friendly representation of the Smart Contract’s functions and data structures. It’s a critical component that allows different programs, like clients and dApps, to understand how to interact with the contract correctly. This is the ABI for our Counter contract:

[
{
"inputs": [],
"name": "count",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "dec",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "get",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "inc",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

This JSON is gonna help our Frontend to communicate with this Smart Contract, encoding methods and arguments to send, and decoding responses. So, how can we make use of it? and most importantly, where can we find this ABI?. We’ll get to this last question later.

Back to our famous inc() call:

const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: "0xB3cAe61C9815cb40141a09330a35971fc453AdE4",
to: '0x60463799Dad8bbF61084aea8DeF20071541cA73A',
data: '0x371303c0' // encoded `inc()` method
}]
})

How did I know that the string 0x371303c0 is the ABI-encoded representation of the method inc()? I used an online tool to find out, but you won’t be doing that when you are building dApps, you just want to call the methods by their name passing whatever list of parameters, right? Basically what you want is to interact with the contract as though it was an object with a bunch methods. Well, thanks to those magic web3 libraries, you will be abstracted from all that encoding and decoding. Take a look at this:

// This is the ABI JSON you saw before. The user-friendly
// representation of our Counter.sol contract
const abi = […]

// Address where the contract is deployed
const contractAddress = '0x60463799Dad8bbF61084aea8DeF20071541cA73A'

// Since we are writing in the Blockchain we need to
// sign the transaction. This signer will be the user's wallet
const signerOrProvider = await web3lib.getSigner()

const counterContract = new web3lib.Contract(
contractAddress,
abi,
signerOrProvider
)

Here we get an instance that we can interact with easily as a normal Javascript object. What’s very important to remember here is that all the methods will be asynchronous. Let’s see how we can read the count:

// In this contract we actually have two ways
// to read the variable `count`, this is because
// this state variable is public, so we get for
// free a method with the same name as the variable

console.log(await counterContract.count())
// or
console.log(await counterContract.get())

This feels definitely more natural, right?, also notice that we don’t need to extract or do any processing of the result. If you remember when I explained the RPC method eth_call, I showed an example of how to call the method get(), which returns a JSON object with some information plus the final result that we had to extract and convert to a number. Here, thanks to the ABI, this result is decoded and given to us already with the right type, in this case it comes as BigInt because the type of this variable is uint (abbreviation for uint256).

How about the methods that require signature? Let’s have a look:

// This line of code will trigger the wallet to open 
// in order to confirm (or reject) this transaction.
// As I already mentioned, this will cost some gas fees
// and you'll be able to configure this to speed up
// the transaction, or pay less and wait longer.
const txHash = await counterContract.dec()

// Once the previous action is confirmed, the transaction is
// signed and sent. `txHash` will have the unique id if you
// want to do something with it, such as giving the user a link
// to the explorer to monitor the transaction, and/or keep
// waiting until it's mined:
const txReceipt = await web3lib.waitForTransaction({hash: txHash})

if (txReceipt.status === 'success') {
alert('You have successfully decreased the counter.')
} else {
// Transaction has been reverted for some reason.
// Whatever gas spent up until the point the failure
// has been deducted from the user initiating this transaction.
// The remaining not used gas is refunded.
alert('You could not decrease the counter.')
}

There is one remaining question: where do I find this ABI? it’s part of the compilation artifacts, when you build your Smart Contracts, then it’s up to you to decide how you import them. You could setup a pipeline as part of the build process that copies them into the Frontend’s folder or some other mechanism so you don’t have to manually bring them.

As a side note, most of these web3 libraries support a more readable representation of the Smart Contract. You might not even need to import the ABIs at all, but instead you pass whatever methods you are gonna need when instantiating the contract as this is definitely more easy to understand. Take a look:

const counterContract = new web3lib.Contract(
contractAddress,
[
// This instance will be used to call these three methods:
'function get() public view returns (uint)',
'function inc() public',
'function dec() public'
],
signerOrProvider
)

One last detail: Big Numbers 🤯

Before I wrap up this article, there is something else quite important to mention, something that many developers coming from Web2 might not be very familiar with: in Web3 numbers are simply too big. Once I got myself into troubles because of this. Basically I broke production, but that’s another story 😅.

Solidity doesn’t support floating-point numbers due to the deterministic nature required for blockchain computations. The precision and rounding behavior of floating-point arithmetic can vary between machines and compilers. This could potentially lead to consensus issues in a blockchain environment, where all nodes must agree on computation results. The work around is multiplying all decimal numbers by some large factor, according to the precision required, and dividing them again when needed, so it’s common that Smart Contracts use really big numbers. I mean, just one Ether is represented as 1000000000000000000 wei—being wei the smallest denomination of Ether — . That’s a big integer, right?. Javascript, on the other hand, can only safely represent integers between -(2⁵³-1) and 2⁵³-1. You can find this number in the constant Number.MAX_SAFE_INTEGER.

In order to work with these large numbers, some web3 libraries come with built-in objects like BigNumber, which are meant to handle them, allowing to perform math operations on them without suffering from a loss of precision. So, big numbers returned by a Smart Contract are wrapped in these objects. Others, more modern libraries, make use of the built-in type BigInt, which was introduced in ES2020.

Wrapping up

It’s been almost a year since I started digging into Web3 development. I bumped into a lot of walls and suffered through different challenges on the way, but no one will ever take away that learning. Web3 is, without a doubt, a very exciting topic, and quite refreshing after lots of years in Web2. I highly recommend the transition, or at least learn and catch up with all these technologies. Get familiar with it, because Web3 is here to stay. It is still in an early stage, but will definitely evolve and improve — I’m looking at you Account Abstraction — . It feels like it’s the natural evolution of Internet.

If you liked this article, please hit that 👏 button 😉

--

--

Francisco Ramos

Machine and Deep Learning obsessive compulsive. Functional Programming passionate. Frontend for a living