How to interact with smart contracts in JavaScript
In this tutorial, we are going to learn how to interact with any smart contract on Ethereum or any EVM chain with JavaScript and the Web3 library.
First of all, you're going to need the address of the smart contract you want to interact with.
You can find it on the blockchain explorer of the network you're using. If you use Ethereum, the explorer is etherscan.io.
If you deployed the contract yourself, you might already have the address.
For this article, we are going to use the smart contract of the Bored Ape Yacht Club NFT collection as an example. The address is: 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D
Once you have the address of the smart contract, there are 2 types of methods that you can call:
- Methods that read the smart contract's state
- Methods that change the smart contract's state
You can get a full list of both in the blockchain explorer:
Now, we're going to need to construct an ABI which is an interface that defines how the functions work so you can interact with it.
To make that ABI, you need to get all the functions that you want to use in the smart contract. In our example, we are going to use:
MAX_APES
– that returns the maximum amount of NFTs that can be mintedbalanceOf
– that checks the amount of NFTs an address hasmintApe
– that mints an NFT
To make the ABI, you can either go to the "Code" section of the "Contract" tab on the image above and scroll down to "Contract ABI". From there you can copy the full contract ABI and simply remove what you're not using.
It's good to know how it works so I'll explain step-by-step how to create your own ABI JSON.
First, here is how to make the ABI for a constant like the MAX_APES
(taken from the smart contract directly):
{
"inputs":[],
"name":"MAX_APES",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "view",
"type": "function"
}
Above you can see that:
- we define the
inputs
which is what the function takes as input parameters, here it's nothing as it doesn't take anything in parameters, it's a constant name
is the name of the functionoutputs
is what it returns. For every variable returned, we define aninternalType
(which is optional), thename
of the variable and the actualtype
that is returnedtype
is the type of variable it is, here it's a functionstateMutability
tells if the function mutates or not the state of the smart contract. It is optional.
And we could have also set "constant": true
to say it's a constant. Other functions that are not constants will have "constant": false
.
Next, functions that are not constants and that get input parameters like balanceOf
:
{
"inputs": [{
"internalType": "address",
"name": "owner",
"type": "address"
}],
"name": "balanceOf",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "view",
"type": "function"
}
You can see that we use the same properties, except that not, there is an input parameter of type address
called owner
. This will change how we call the function as we will need to pass that parameter in.
Lastly, for payable
function, it will be slightly different. Payable functions like mintApe
are functions that require you to send Ethereum as you call them. In our case, the amount to send is the mint price of their NFTs. Since they're all minted the function won't work but it's interesting to see how it works:
{
"inputs": [{
"internalType":"uint256",
"name":"numberOfTokens",
"type":"uint256"
}],
"name":"mintApe",
"outputs":[]
"stateMutability":"payable",
"type":"function"
}
Here, the difference is that stateMutability
is now "payable"
since it's a payable function. You can also set "nonpayable"
if the function changes the state but is not payable.
Now, let's put it all together:
const ABI = [
{
"inputs":[],
"name":"MAX_APES",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{
"internalType": "address",
"name": "owner",
"type": "address"
}],
"name": "balanceOf",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{
"internalType":"uint256",
"name":"numberOfTokens",
"type":"uint256"
}],
"name":"mintApe",
"outputs":[]
"stateMutability":"payable",
"type":"function"
}
]
Now that we have the ABI, let's actually interact with the contract.
First, install Web3JS: npm install web3
As always, you need a provider to use web3. Either an API URL like the ones Infura provide or have a wallet connected to your website. In this example, we're going to use an Infura API URL. You can learn how to get an Infura API URL here.
To start using the functions in our ABI, we create an Contract
instance like this:
import Web3 from 'Web3'
const INFURA_URL = "YOUR URL HERE"
const web3 = new Web3(new Web3.providers.HttpProvider(INFURA_URL))
const contractAddress = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"
// ABI is defined above
const contract = new web3.eth.Contract(ABI, address)
Using that contract instance, we can now call any function defined in the ABI as if it was a normal JavaScript function:
// Getting MAX_APES
const maxApes = await contract.methods.MAX_APES().call()
// maxApes = 10000
// Getting the balance of an address
const addressToCheck = "0xbA23d58c7289AaBF1820cB35bB4b5Bb89170fFfD"
const balance = await contract.methods.balanceOf(addressToCheck).call()
// at the time of writing this article, balance = 3
Now, contract methods that change the state of the smart contract need to be part of a transaction because they consume gas. If you don't send them in a transaction, it won't work.
Here is an example with mintApe
(we set the mint price to an example value):
const nftsToBuy = 1
const mintPrice = 2 // here we set the mint price to 2 ETH
// convert the amount in ETH to Wei to pass it to the contract
const payableAmount = Web3.utils.toWei(
`${mintPrice * nftsToBuy}`,
'ether'
)
web3.eth.sendTransaction({
from: fromAddress, // address of the connected wallet here
to: address, // smart contract address here
value: payableAmount
data: contract.methods.mintApe(payableAmount,nftsToBuy).encodeABI(),
})
.on((receipt) => {
// do somehting when the transaction passed
// receipt will have all the info about the transaction
})
.on((error) => {
// do something if the transaction failed
})
Note that for the code above to work, you'll need to have an address to send the transaction from. Usually it will be a wallet connected to your website. To learn how to connect a wallet to your website check out this article if you're using React, this one if you want to use Web3Modal to connect any wallet, and this article if you're using vanilla JavaScript.
In the code above, we send a transaction containing our call to the contract method inside the data
property of the transaction. This will indicate the contract what we want to do.
Since we need to send Ethereum when we mint, we added the correct amount of Ethereum to the transaction through the value
property.
If a smart contract function changes the state but is not payable (doesn't require you to send ETH), then you don't set the value
property.
Also note that the address you send the transaction to is always the smart contract.
Last example using the transferFrom
function:
const ABI = [
// define the ABI here using the method explained above
]
// ... create a web3 instance by connecting a wallet ...
const contractAddress = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D"
const contract = new web3.eth.Contract(ABI, address)
const data = contract.methods.transferFrom(
fromAddress,
toAddress,
tokenId, // ID of the NFT, like 1, 524, 888 (it's in the name of the NFT)
).encodeABI()
web3.eth.sendTransaction({
from: fromAddress, // address of the connected wallet here
to: address, // smart contract address here
data,
})
.on((receipt) => {
// do somehting when the transaction passed
// receipt will have all the info about the transaction
})
.on((error) => {
// do something if the transaction failed
})
Alternatively, you can also call functions that mutate the state and send transactions using the .send
method:
const nftsToBuy = 1
const mintPrice = 2 // here we set the mint price to 2 ETH
// convert the amount in ETH to Wei to pass it to the contract
const payableAmount = Web3.utils.toWei(
`${mintPrice * nftsToBuy}`,
'ether'
)
contract.methods.mintApe(payableAmount,nftsToBuy).send({
from: fromAddress, // address of the connected wallet here
to: address, // smart contract address here
value: payableAmount
})
.on((receipt) => {
// do somehting when the transaction passed
// receipt will have all the info about the transaction
})
.on((error) => {
// do something if the transaction failed
})
The object in the parameters of the .send
function are the same as the transaction object, unless you don't need the data
property.
And that's it! 👏
Thanks for reading this article!