After my first article about Solidity basics for JavaScript devs got so much attention, I'm writing a second one!
I'm currently working through a beginner smart contract development book, and now I'm doing the main project, a DApp fundraiser.
The book is written for Truffle, web3.js, and JavaScript, and I replaced the tools with Hardhat, Ethers.js, and TypeScript to spice things up a bit.
Here are my last findings that threw me off a bit, so I think they might be interesting for newcomers!
Solidity Events are for the Frontend
Solidity has an event construct/type. It allows you to define specific events for your smart contract that can emit when things you deem interesting.
event MyEvent( uint256 value1, uint256 value2);
function f() public {
emit MyEvent(123, 456);
}
Interesting for whom? For your frontend code!
If I understood it correctly, event data would be stored in the blockchain, but it isn't accessible within smart contracts.
Event data is for listeners from outside the blockchain.
Your frontend can add event listeners for these events, and then, when it starts a transaction, these events will be emitted, and you can do things in the frontend.
smartContract.on("MyEvent", (valueA, valueB) => {
console.log(valueA, valueB);
})
await smartContract.f();
Ethers.js Uses BigNumber
Instead of BigInt
Solidity usually has to handle huge integers, too big for the Number
type of JavaScript. That's why Ethers.js created their type, called BigNumber
, to get around this problem.
Today, modern JavaScript engines all have a BigInt
type that can handle such values no problem, but this wasn't always the case, and Ethers.js wanted to be backward compatible.
I don't know why they didn't use a BigInt
polyfill instead, but at least they offer a method toBigInt()
. But you have to use BigNumber
methods for calculations!
const value1 = ethers.utils.parseEther("1")
const value2 = ethers.utils.parseEther("2")
const result = value1.add(value2)
console.log(result.toBigInt())
Anyway, don't mistake BigNumber
for BigInt
or you'll have a bad time!
Setting the msg
Object from Ether.js
There are some global variables inside your Solidity smart contract generated automatically before your function is called.
One of them is called msg
, and it contains data that isn't explicitly passed via function arguments, like msg.sender
for the address that called the function or msg.value
for the amount of Ether that was sent with the function call.
function f(uint256 arg1, uint256 arg2) public payable {
// These are obviously arguments
uint256 a = arg1 + arg2;
// But where do these come from?
address x = msg.sender;
uint256 y = msg.value;
}
As this data isn't a function argument, how do you pass it to the smart contract from the Ethers.js side?
An overrides object is passed as the last argument to such a (payable) function, after all the regular arguments. Other values, like msg.sender
are implicitly set on the smart contract side of things.
const overrides = {value: 123}
await smartContract.payableFunction(arg1, arg2, overrides)
Multiple returns will become an Array in Ethers.js
Solidity allows returning multiple values from one function.
function f() public returns(uint256 a, uint256 b) {
return (123, 456);
}
I saw some examples, seemingly for web3.js, that would use an object as a return value on the JavaScript side.
const {a, b} = await smartContract.f();
This didn't work for me; I used an array to extract the return values depending on their position.
const [a, b] = await smartContract.f();
Using Waffle with Chai for Tests
The book I'm reading used low-level assertions with some try-catch constructs to test smart contract-specific behavior. I guess Waffle wasn't a thing back then.
To test events, you can use an asynchronous call to expect
.
it("emits", async () => {
await expect(smartContract.f()).to.emit("EventType")
})
You can use an asynchronous call to expect
with reverted
to test that your contract reverts correctly.
it("emits", async () => {
await expect(smartContract.f()).to.be.revertedWith("Error Message")
})
Summary
Web3 is an interesting topic, and Solidity is certainly a different language than I expected. It's simple in the sense that JavaScript is simple, but the devil lies in the detail.
I hope I could clear some things up for you.
Top comments (2)
Yes, the BigNumber / BigInt thing is a bit of a pain, particularly when dealing with async results, as one often has to with Ethers.js. e.g.
const supply = (await contract.totalSupply()).toBigInt()
. It's pretty tedious.I suggest either converting to BigInt early (for more modern JS) in the rest of the code, or to mostly ignore BigInt and just stick with BigNumber, like some dev from 2 years ago lol.
Adapting your example above to first approach:
Fortunately, Ethers.js now considers BigInt to be "BigNumberIsh", so it accepts BigInts as arguments for
uint
etc.First snippet there could also be done as:
const supply = await contract.totalSupply().then(BigInt)
or
const supply = BigInt(await contract.totalSupply())
.