I became more interested in Web3 after realizing its potential to revolutionize the world. I wanted to go beyond a simple “hello world”, so I’m documenting my adventure into Web3 as an engineer and founder. I will share my experiences, observations, and lessons learned through this documentation, and I want to encourage other engineers, entrepreneurs, and fans to explore Web3’s potential.
Here we will explore some basics of blockchain and a developer’s framework called scaffold-eth
. We will attempt to build a simple game involving a Solidity contract with React frontend, which is included in scaffold-eth
.
What’s in this article:
Immutability and Cost
Traditional programming and Web3 programming don’t differ much, except that you’d need to understand a few key concepts: immutability and cost. Immutability is not a difficult concept to understand, but you’d need to be careful when rolling out the app – there is no going back once it’s out; every record made on the chain stays there forever (in most cases). This immutability connects to the second point: Cost.
Computers have become so efficient that local consumption of electricity is negligible. However, this is not the case for blockchains, as this computation propagates to the immutable ledger and the cost of making an entry into this permanence is somewhat costly.
To some extent, this immutability and cost remind me of programming in the 80s on instruction cards, or maybe GameBoys, where you’d need to get creative to make your code efficiently performant and small.
The Stack
Picking your stack is not easy. Different blockchains, different tooling – the entire thing is worse than javascript fatigue, but fret not. Let’s start with Ethereum since it’s one of the first programmable blockchains, and many have evolved from it. A simple visit to Ethereum Developer’s page will give you an idea of what tooling is available to you.
The biggest hurdle to clear is understanding what goes into the stack.
- Frontend - what the user sees and what they usually interact with
- Backend server - where you are hosting your frontend app
- Solidity Backend - your smart contracts
- Blockchain - the chain where you would deploy your contracts to.
- Blockchain proxy - how your wallets communicate with the chain
As you can see as a newcomer, you can have extreme difficulty setting up your development stack just to even get started. Forget about deploying to production by yourself.
scaffold-eth
scaffold-eth
has been kind to me. Once you clone this repository, a set of tools will facilitate you from the development of your app to deploying it to production. It’s very well put together by @austintgriffith
, who is also on the Ethereum Foundation. I would recommend anyone who is getting into Solidity development to try this tool and master the basics, then go further to find their flavor of tooling choices.
Guided Learning
There are several guided tutorials and challenges available online. The easiest has been Speedrun Ethereum. It takes you through a set of challenges, but most importantly, it lets you create a public web3 portfolio, which you would be able to showcase later on. It has a set of puzzles that you have to figure out, rather than mindlessly copy-pasting answers from the tutorial, which is quite challenging if you’re not a programmer. You do have to figure out the solutions yourself.
If you’re more of a zombie learner, perhaps it’s worth checking out Crypto Zombie, but I’ve had my share of zombies in my life. This tutorial teaches the mechanics of Solidity, without getting into details about setting up your local stack and deploying to the blockchain. (this might have changed)
Let’s dive into scaffold-eth and create a game.
King’s Pot, The Game
The initial steps of Speedrun Ethereum helped me get started with the setup of the stack, but now I’ve decided to venture off and test out the concepts in real life.
We will be building an easy gambling game. The rules are simple: you have a pot, into which participants would throw their money. A counter counts down to a set time limit, after which, the last person to put the money into the pot can claim the winnings. The catch is that every time someone puts the money into the pot, the timer gets extended and gives more chances for others to become the winner.
Getting Started
Let’s get the simple things out of the way. We need to clone scaffold-eth and get the services running for our development.
git clone https://github.com/scaffold-eth/scaffold-eth.git kings-pot
cd kings-pot
yarn install
yarn chain # starts the local blockchain for development
# then open a new window and start the web server
yarn start
The Contract
scaffold-eth
gives you a very nice interface to play around with your contract once it’s deployed, so let’s work on the contract before diving into the front end. The crux is to get the contract working without any bugs.
The scaffolding comes with a default contract in packages/hardhat/contracts/YourContract.sol
, so we can reuse that or create our own. I’ve decided to create my own contract called TimedLottery
, so I’ve replaced all instances of YourContract
with TimedLottery
in the project.
Let’s insert a blank contract:
pragma solidity ^0.8.0;
//SPDX-License-Identifier: MIT
contract TimedLottery {
constructor() {}
receive() external payable {}
fallback() external payable {}
}
Constants
Let’s add all the necessary constants and variables based on our requirements. Your contract should look something like this:
pragma solidity ^0.8.0;
//SPDX-License-Identifier: MIT
contract TimedLottery {
address payable public lastDonor; // the last person to put into the pot; becomes the winner of the pot
uint256 public potSize; // track the size of the pot
uint256 public timer; // track the timestamp when timer runs out
uint256 public constant timerReset = (24 hours) * 6 * 30; // initial time when the timer reset => timer = now() + timerReset
uint256 public constant timerIncrement = 15 minutes; // how much time to add when someone transfers eth into the pot
constructor() {}
receive() external payable {}
fallback() external payable {}
}
Let’s break this down further:
lastDonor
: We will use this to track the last sender of eth to the contract. We don’t need to track all the transfers since we only need oneaddress
to determine the winner.potSize
:uint256
type is BigNumber type which will track the size of the winnings. This is an unsigned type, so we can only have zero or positives.timer
: we will set the default time for this further, but essentially this variable stores the timestamp representing the future block time, which will be used to track when the lottery ends.timerReset
andtimerIncrement
are both used to store fixed seconds which will be used for contract manipulation so we don’t hardcode anything inside the functions.
Deciding when the lottery ends
Let’s set the default values in the constructor
. This method is called when the contract is deployed by you. The only variable that we need to set is timer
to specify when the lottery expires.
constructor() {
timer = block.timestamp + timerReset;
}
Here we get the current block’s timestamp and append our pre-determined expiration time. (6 months). You can change this length to something shorter for development.
Receiving funds
Now that we are tracking when the lottery ends, let’s create a function so that our contract can receive funds. Let’s create a fund
function, which will handle all the logic for receiving the funds.
Here, we need to do the following:
- Store the address where the funds are coming from in the
lastDonor
- Increment
potSize
with the amount being sent. - Increment the timer.
We need to make sure to specify external
and payable
for this function, which will allow for this to be run from the outside and attach a value
to it.
function fund() external payable {
lastDonor = payable(msg.sender);
potSize += msg.value;
timer += timerIncrement;
}
The above looks good, but there are a few edge cases. What if the sender forgets to attach an amount. It’s possible to run this function without attaching any value
and become the winner of the pot. To counter this, let’s add some validations:
require(msg.value > 0, "funding amount must be greater than 0.");
Additionally, let’s make sure we stop accepting funds once the lottery is over:
require(block.timestamp < timer, "Funding window is closed.");
This essentially checks that the current timestamp of the block is smaller than the timer that we’ve set. Your fund
function will look something like this:
function fund() external payable {
require(msg.value > 0, "funding amount must be greater than 0.");
require(block.timestamp < timer, "Funding window is closed.");
lastDonor = payable(msg.sender);
potSize += msg.value;
timer += timerIncrement;
}
Claiming the Winnings
Now that we can receive funds into the contract and track the last sender, let’s see how we claim the funds. Let’s create a claimPot
function, which can be called when the timer runs out. Similar to fund
, we need to make this function external
so outsiders can run it.
In this function we need to do the following:
- Make sure only the winner is calling it.
- Transfer the amount in the pot to the winner.
- Clear the pot and set it to 0.
- Reset the timer to restart the game.
The function will look something like this:
function claimPot() external {
require(block.timestamp > timer, "Claim window is closed.");
require(potSize > 0, "Pot is empty.");
require(msg.sender == lastDonor, "Only the winner can claim the potSize.");
uint256 amount = potSize;
potSize = 0;
lastDonor.transfer(amount);
// reset timer
timer = block.timestamp + timerReset;
}
That’s it! The ready contract should look something like this:
pragma solidity ^0.8.0;
//SPDX-License-Identifier: MIT
contract TimedLottery {
address payable public lastDonor; // the last person to put into the pot; becomes the winner of the pot
uint256 public potSize; // track the size of the pot
uint256 public timer; // track the timestamp when timer runs out
uint256 public constant timerReset = (24 hours) * 6 * 30; // initial time when the timer reset => timer = now() + timerReset
uint256 public constant timerIncrement = 15 minutes; // how much time to add when someone transfers eth into the pot
constructor() {
timer = block.timestamp + timerReset;
}
function fund() external payable {
require(msg.value > 0, "funding amount must be greater than 0.");
require(block.timestamp < timer, "Funding window is closed.");
lastDonor = payable(msg.sender);
potSize += msg.value;
timer += timerIncrement;
}
function claimPot() external {
require(block.timestamp > timer, "Claim window is closed.");
require(potSize > 0, "Pot is empty.");
require(msg.sender == lastDonor, "Only the winner can claim the potSize.");
uint256 amount = potSize;
potSize = 0;
lastDonor.transfer(amount);
// reset timer
timer = block.timestamp + timerReset;
}
receive() external payable {}
fallback() external payable {}
}
Deploy the contract
Let’s deploy the contract with yarn deploy
Once deployed, we can open up the interface, go to debug tab (http://localhost:3000/debug) and play around with the contract. Make sure to fund your frontend wallet with a local faucet, and try to transfer some funds and verify that potSize
grows. Once the timer
has run out, you can run the claimPot
function. timerReset
is set to 6 months, and we don’t have that kind of luxury, so make sure to change that to something reasonable like 3 minutes.
Making it reusable
Hold your horses! or Hold your Mario karts! What happens when the last person doesn’t claim booty? We need to make sure the game restarts. Why don’t we make it so the loot rolls over to the next game, and we keep some for ourselves?
Let’s add a rolloverPot
function and update our fund
function. Here, we take 20% off the leftover loot, and take it for ourselves haha!! cha-ching!
Make sure to track the address of the wallet that deployed the contract and extract the percentage of your cut into a variable.
rolloverPot
function:
uint256 public constant ownerPercentage = 20;
constructor() {
owner = payable(msg.sender);
timer = block.timestamp + timerReset;
}\
function rolloverPot() internal {
uint256 ownerAmount = (potSize * ownerPercentage) / 100;
uint256 rolloverAmount = potSize - ownerAmount;
owner.transfer(ownerAmount);
potSize = rolloverAmount;
timer = block.timestamp + timerReset;
}
fund
:
function fund() external payable {
require(!(block.timestamp > timer && block.timestamp < timer + claimPeriod), "Funding is closed during claim period");
require(msg.value > 0, "funding amount must be greater than 0.");
// rollover
if (potSize > 0 && block.timestamp > timer + claimPeriod) { rolloverPot(); }
lastDonor = payable(msg.sender);
potSize += msg.value;
if (timer < block.timestamp) {
// restart the game
timer = block.timestamp + timerReset;
} else {
// regular increment
timer += timerIncrement;
}
}
Frontend
We’ve verified that our contract is working, let’s add some custom frontend.
scaffold-eth
provides us with a template that we can reuse, so let’s repurpose packages/react-app/src/views/Home.jsx
and remove any boilerplate code. Should look something like this:
import React from "react";
function Home() {
return (
<div>King's Pot</div>
);
}
export default Home;
Display Pot Size
We need to read the contract values to display how much ETH has been transferred into the contract. This can be done by using useContractReader
from eth-hooks
, which is part of the scaffold-eth
framework.
Use import { useContractReader } from "eth-hooks";
and make sure to pass in readContracts
into this view in packages/react-app/src/App.jsx
. We can read the potSize
from our contract using const potSize = useContractReader(readContracts, "TimedLottery", "potSize");
.
However, our contract does not return an integer, but rather a BigNumber
. They are used here to prevent the values from overflowing in javascript. We don’t have much use for this in BigNumber
form, so we’d need to convert this to something more meaningful, and ethers
has a utility function to format BigNumber
into Ethereum values: const potSize = !potSizeBigNumber ? 0 : ethers.utils.formatEther(potSizeBigNumber);
.
App.jsx
should have something like the following:
<Route exact path="/">
<Home readContracts={readContracts} />
</Route>
and Home.jsx
should be:
import React from "react";
import { ethers } from "ethers";
import { useContractReader } from "eth-hooks";
function Home({ readContracts }) {
const potSizeBigNumber = useContractReader(readContracts, "TimedLottery", "potSize");
const potSize = !potSizeBigNumber ? 0 : ethers.utils.formatEther(potSizeBigNumber);
return (
<div>King's Pot: {potSize} ETH</div>
);
}
export default Home;
Funding the pot
Let’s figure out a way to call the fund
function from our front end. Similar to useContractReader
, there is a writeContracts
, which provides us access to executing Solidity functions from the web. Also, we need to make sure to use tx
helper, which will provide meaningful user feedback based on the result of the transaction.
Pass in tx
and writeContracts
to our Home
in App.jsx
<Route exact path="/">
<Home
readContracts={readContracts}
tx={tx}
writeContracts={writeContracts}
/>
</Route>
Now we can update our Home.jsx
. We would need to add 2 components to our UI – an input field for ETH, and a button that will call the fund
function. We can use EtherInput
from our component library, and Button
from antd
library.
When a value is entered into EtherInput
, we use React
’s useState
helper to update the state for fundAmount
.
When the button is pressed, we get our contract via writeContracts
and call the fund
function with values
set to fundAmount
. Then we wrap everything in a tx
(Transactor
) and attach an update
callback to handle notifications after the function is executed.
There are a lot of things that are going on under the hood of this template, and I encourage you to trace the code and see how scaffold-eth
is built. Your Home.jsx
should look something like this:
import React, { useState } from "react";
import { ethers } from "ethers";
import { useContractReader } from "eth-hooks";
import { Button } from "antd";
import { EtherInput } from "../components";
function Home({ readContracts }) {
const potSizeBigNumber = useContractReader(readContracts, "TimedLottery", "potSize");
const potSize = !potSizeBigNumber ? 0 : ethers.utils.formatEther(potSizeBigNumber);
const [fundAmount, setFundAmount] = useState(ethers.utils.parseEther("0"));
return (
<div>
<div>King's Pot: {potSize} ETH</div>
<EtherInput
onChange={amt => {
try {
const newValue = amt.startsWith(".") ? `0${amt}` : amt;
setFundAmount(ethers.utils.parseEther("" + newValue));
} catch (e) {
console.error(e);
setFundAmount(ethers.utils.parseEther("0"));
}
}}
/>
<Button
onClick={async () => {
/* notice how you pass a call back for tx updates too */
const result = tx(writeContracts.TimedLottery.fund({ value: fundAmount }), update => {
console.log("📡 Transaction Update:", update);
if (update && (update.status === "confirmed" || update.status === 1)) {
console.log(" 🍾 Transaction " + update.hash + " finished!");
console.log(
`⛽️ ${update.gasUsed}/${update.gasLimit || update.gas} @ ${
parseFloat(update.gasPrice) / 1000000000
} gwei`,
);
}
});
console.log("awaiting metamask/web3 confirm result...", result);
console.log(await result);
}}
>
Add to Pot!
</Button>
</div>
);
}
export default Home;
When you fund the pot, verify that your ETH balance goes up.
Displaying the Countdown Timer
Let’s try to indicate when the timer will run out, so other players know if they still have a shot at becoming a winner.
Utilizing the same method, by reading contract values with useContractReader
, let’s read our timer timestamp:
const timer = useContractReader(readContracts, "TimedLottery", "timer");
We can subtract the timestamp from our current timestamp to get the number of seconds left until the timer expires.
const now = new Date();
const date = new Date(timer.toNumber() * 1000);
const secs = (now.getTime() - date.getTime()) / 1000;
And we can display the time using something like this:
<span>{secs} seconds left!</span>
Refreshing the timer
You’ll notice that your seconds only get updated every so often. This is because React’s state is not being refreshed since nothing is actually being triggered. Let’s rewrite this using useEffect
and use setInterval
to refresh our clock. For this to be performant, let’s extract it into its own component, name it PotTimer
and put it in the components
folder. Don’t forget to add an import to components/index.js
.
In this component, we want to do the following:
- Read the
timer
value ofTimedLottery
and store it in a state variable - Get the difference between the
timer
timestamp and the current time - Format the seconds into a readable
HH:MM:SS
format - Wrap and refresh this function every 1 second to render up-to-date values
Should look something like this:
import { useContractReader } from "eth-hooks";
import React, { useCallback, useState, useEffect } from "react";
import { POT } from "../constants";
export default function PotTimer({ readContracts }) {
const [secsLapsed, setSecsLapsed] = useState(-999);
const timer = useContractReader(readContracts, "TimedLottery", "timer");
const claimPeriod = 5 * 60;
function secsToHMS(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60);
const pad = num => (num < 10 ? "0" + num : num);
return `${pad(hours)}:${pad(minutes)}:${pad(remainingSeconds)}`;
}
const refreshClock = useCallback(() => {
if (timer) {
const nowDate = new Date();
const date = new Date(timer.toNumber() * 1000);
const secs = (nowDate.getTime() - date.getTime()) / 1000;
setSecsLapsed(secs);
}
}, [timer]);
useEffect(() => {
const timerId = setInterval(refreshClock, 1000);
return function cleanup() {
clearInterval(timerId);
};
}, [refreshClock]);
if (secsLapsed < 0) {
return <span>{secsToHMS(secsLapsed * -1)} seconds left!</span>;
}
if (secsLapsed - claimPeriod < 0) {
return <span>{secsToHMS((secsLapsed - POT.claimPeriod) * -1)} seconds left to claim!</span>;
}
return <div>You can start your journey</div>;
}
Now we can use this component in our Home.jsx
like so:
<PotTimer readContracts={readContracts} />
Testing
We can test this game by opening two windows pointed to http://localhost:3000/
. Make sure they are using different wallets. You can do this by opening one in incognito mode.
You can check out a working version here running on Sepolia testnet. The source code for the above is available here: https://github.com/yurikoval/kings-pot/tree/v1.0.0.