My first taste of building a Web3 app with Solidity, React and TypeScript

Francisco Ramos
17 min readJan 26, 2023

--

Lately I have a lot of time on my hands. Centralized exchanges are blowing up everywhere, not doing so great during these difficult times in the Crypto space, and unfortunately I was affected by this. If 2022 has taught us something is that centralization is not really the way to go.

This is the beginning of a new era for Internet. Transparency, control over your data and decentralization are some of the key aspects of Web 3.0: the re-invention of Internet. I’m not gonna explain here the differences with web2, nor the evolution from web1. I leave that for the reader to do the research on. There is plenty of information out there explaining what web3 really is, history, evolution, and why it matters.

With this free time I decided to dig deeper into the subject from the development point of view, learn and understand the different pieces, languages, tools and frameworks, as well as architecture, required to build and deploy a decentrilized application or dApp. Basically, transitioning from traditional web2 development to web3.

I’m originally a Frontend developer, although I’ve been doing a lot of Backend work as well as DevOps tasks, which gave me the skills to build and deploy applications from A to Z. Personally I find this “generalization” more rewarding than building just Frontend. Don’t get me wrong, I do love Frontend, the Browser is an amazing platform with a rich API, and the whole ecosystem around Frontend is so huge that it’s hard to get bored. There is always so much to learn… but I feel like it’s time for me to move on, and evolve along with the Internet. Web3 is here to stay, and I’m here to soak up everything about it.

The project: Lottery Contract

As part of my routine when learning something new, either a technology, language, architecture, pattern, library or framework, I decided to pick an idea and execute it.

Inspired by one of the projects proposed in the course Ethereum and Solidity: The Complete Developer’s Guide, I decided to build it using the latest web3 technologies and improve it with a more engaging UI. The idea behind this game is to have a prize pool and a list of participants. Each participan will enter the game sending an amount of ether to the contract. The manager, or owner of the contract, decides when to pick a winner. The total ether locked in the contract is then transferred to the lucky one.

Ethereum Network

There are a few blockchains out there where to build dapps on. Since I’m new to this world, I decided to go easy and pick the one where I know the community is big and I’ll find a lot of resources on: Ethereum and EVM compatible chains. This meant also that I had to learn a new language, its native language: Solidity. I love learning new programming languages so this was the most fun part.

If you know Python and Javascript, Solidity is a pice of cake, since it’s got a lot influence from these two.

Solidity is an object-oriented, high-level language for implementing smart contracts. Smart contracts are programs which govern the behaviour of accounts within the Ethereum state. It’s statically typed, supports inheritance, libraries and complex user-defined types among other features.

Tooling

This was the hardest part. The ecosystem is huge here. From frameworks to Frontend libraries, other libraries and SDKs, test blockchains and node providers, wallets, decentralized data storage, and a long etc of tooling that you need in order to build, test and deploy web3 applications. It reminds me of Frontend tooling, which can be overwhelming for newcomers.

Frontend

I decide to use my usual tech stack:

I like using antd UI and tailwind css when working on personal projects because I know them well and they help me to build UI very quickly.

Backend/Blockchain

For this part I went with the following tools:

Truffle is a great framework that helps with building, testing and deploying smart contracts. There is another popular framework that’s gaining a lot of momentum, which seems to be the preferred one nowadays, and which I’ll be using in my next project: Hardhat

Web3.js is a collection of libraries that allow you to interact with a local or remote ethereum node using Javascript. This library is pretty much the only option you have when using Truffle though. I believe it’s difficult to move to another library. Another popular option is ethers.js. By looking at its npm package page, it looks like it’s overtaking web3.js in weekly downloads, so even though it’s newer, its popularity has passed web3.js. Same as with Hardhat, I’ll be using ethers.js in my next project.

Testing

We have a few options here:

Ganache is nothing more than an Ethereum blockchain running in your local machine, so you can deploy, run and therefore test smart contracts locally and easily. You can either install the UI or CLI version. Even though I’m a CLI type of developer, for convenience I used the UI version in this first project.

With Truffle, you can test your smart contracts, written in Solidity, using Javascript, and this is the reason why you can use Mocha, which is a Javascript testing framework. Along with it, in order to write those tests in Typescript, I included ts-node in the testing pipeline.

Wallet

We need one last important piece when building and using any decentralized application: a wallet. In web3, a wallet is like your identity, and we use it to interact with dapps. We are no longer granted access to the app through a centralized database login, instead we connect our wallet to the app, and that’s it, we’re logged in. This works for any dapp, so no need to go through a registration process, setting up credentials, email confirmation, etc. There is way more here about wallets, but it’s not the purpose of this article to explain them in detailed. There are many articles out there if you want to know more about them.

There are quite a few wallets, browser extensions, so you can easily connect them to dapps. Probably the most famous one is MetaMask

Executing the idea

After a lot of research, courses, playing around and building contracts in Remix IDE (highly recommended if you’re learning Solidity, since it requires no setup, it all runs in the browser, including blockchain), I decided to move on to the real world of Github and VSCode to make this happen. We’re gonna build a web3 application, Frontend and Backend, running on Ethereum network.

First thing you need is a blockchain running in your local machine. As I mentioned earlier on, Ganache is your friend. Running the UI will give you 10 accounts with 100 ETH each.

Second thing is a wallet extension in your browser to interact with the application. For that MetaMask is one of the best. The next step is to import the accounts provided by Ganache into your wallet and connect to the local network. You can follow this tutorial to do so.

Now, let’s get straight into the code

Truffle Project

As mentioned, I’m using Truffle as my framework for smart contracts development. Configuration is quite straight forward. After npm install truffle you can now create a bare project by running: npx truffle init. This will create the following project structure:

This is my truffle-config.js for this specific project:

// Will helps us to run the test written in Typescript
require('ts-node').register({
files: true,
project: './tsconfig.test.json',
})

// Grabs environment variables from .env file
require('dotenv').config()

const HDWalletProvider = require('@truffle/hdwallet-provider')

const { MNEMONIC, NEXT_PUBLIC_PROJECT_URL } = process.env

module.exports = {
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a managed Ganache instance for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache, geth, or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
development: {
host: '127.0.0.1', // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
},

// Useful for deploying to a public network.
// Note: It's important to wrap the provider as a function to ensure truffle uses a new provider every time.
goerli: {
provider: () => new HDWalletProvider(MNEMONIC, NEXT_PUBLIC_PROJECT_URL),
network_id: 5, // Goerli's id
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true, // Skip dry run before migrations? (default: false for public nets )
},
},

// Set default mocha options here, use special reporters, etc.
mocha: {
// timeout: 100000
},

// Configure your compilers
compilers: {
solc: {
version: '0.8.17', // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: "byzantium"
// }
},
},
}

The Contract

Second step was implementing the contract: Lottery.sol. This is where you get your hands dirty with the Solidity programming language. By looking at the code, the functionality is quite clear, and if you come from the C++, Javascript or Python world, then this will look very familiar to you:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "@openzeppelin/contracts/utils/Strings.sol";

/**
* @title Lottery game
* @author Francisco Ramos
* @notice Simple web3 game based on Udemy course
* https://www.udemy.com/course/ethereum-and-solidity-the-complete-developers-guide/
*/
contract Lottery {
// Stores the owner of the contract
address public owner;

// keeps track of the people who enter the game
address payable[] public players;

// We use it to emit some details about the pickWinner transaction
event WinnerPicked(
uint index,
uint prize,
address winner
);

/**
* @dev Stores the address of the person deploying the contract
*/
constructor() {
owner = msg.sender;
}

/**
* @dev Enforces a minimun amount of ether to be sent to a function
* @param value The minimum amount to send
*/
modifier minimum(uint value) {
string memory requiredMsg = string.concat("The minimum value required is ", Strings.toString(value));
require(msg.value >= value, requiredMsg);
_;
}

/**
* @dev Makes sure the owner is the only one who can call a function
*/
modifier restricted() {
require(msg.sender == owner, "Only the owner of this contract can call the function");
_;
}

/**
* @dev Will be called by the player who enters de game sending ether
* and makes sure he/she is sending a minumun of 0.01 ether
*/
function enter() public payable minimum(.01 ether) {
players.push(payable(msg.sender));
}

/**
* @dev Generates a pseudo random number
* https://medium.com/0xcode/hashing-functions-in-solidity-using-keccak256-70779ea55bb0
* https://docs.soliditylang.org/en/v0.8.17/abi-spec.html
* @return index of the player within our list
*/
function random() private view returns (uint) {
return uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, players)));
}

/**
* @dev Gets the list of players currently in the game
* @return players
*/
function getPlayers() public view returns (address payable[] memory) {
return players;
}

/**
* @dev Called by the manager, it picks a winner
* emitting WinnerPicked event
*/
function pickWinner() public restricted {
// Compute the (pseudo)random index of the winner
uint index = random() % players.length;

uint prize = address(this).balance;
address payable winner = players[index];

// Transfer the total amount to the winner
winner.transfer(prize);

// Empty the list of players
players = new address payable[](0);

// Emit event with details of the result
emit WinnerPicked(
index,
prize,
winner
);
}
}

Testing the contract

This is the fun part, testing the contract’s business logic (I personally love unit testing). The cool thing here is that you can do so using Javascript, and with the help of TypeChain, you are able to generate the types in order to have IDE support and everything statically typed. If you want to learn more about how to use TypeScript to write tests in Truffle, this article explains the whole setup, or you can also check out this example. Here I’m trying to cover all the scenarios we could have while interacting with this contract:

import type {
LotteryInstance,
LotteryContract,
} from '../types/truffle-contracts/Lottery'

const Lottery: LotteryContract = artifacts.require('Lottery')

let lotteryInstance: LotteryInstance

beforeEach(async () => {
lotteryInstance = await Lottery.new()
})

contract('Lottery', (accounts) => {
it('allows one account to enter', async () => {
await lotteryInstance.enter({
from: accounts[0],
value: web3.utils.toWei('0.01', 'ether'),
})

const players = await lotteryInstance.getPlayers({
from: accounts[0],
})

assert.equal(accounts[0], players[0])
assert.equal(players.length, 1)
})

it('requires a minimum value', async () => {
try {
await lotteryInstance.enter({
from: accounts[0],
value: web3.utils.toWei('0.0099', 'ether'),
})

assert.fail('Should have raised exception "minimum value required"')
} catch (e) {
assert.ok(e, 'Exception "minimum value required" has been raised')
}
})

it('allows multiple accounts to enter', async () => {
await lotteryInstance.enter({
from: accounts[0],
value: web3.utils.toWei('0.02', 'ether'),
})

await lotteryInstance.enter({
from: accounts[1],
value: web3.utils.toWei('0.03', 'ether'),
})

await lotteryInstance.enter({
from: accounts[2],
value: web3.utils.toWei('0.04', 'ether'),
})

const players = await lotteryInstance.getPlayers({
from: accounts[0],
})

assert.equal(accounts[0], players[0])
assert.equal(accounts[1], players[1])
assert.equal(accounts[2], players[2])

assert.equal(3, players.length)
})

it('only the owner can call pickWinner method', async () => {
await lotteryInstance.enter({
from: accounts[0],
value: web3.utils.toWei('0.02', 'ether'),
})

await lotteryInstance.enter({
from: accounts[1],
value: web3.utils.toWei('0.02', 'ether'),
})

await lotteryInstance.enter({
from: accounts[2],
value: web3.utils.toWei('0.02', 'ether'),
})

try {
await lotteryInstance.pickWinner({
from: accounts[1], // accounts[1] is not the owner
})

assert.fail('Should have raised exception "Not the owner"')
} catch (err) {
assert.ok(err, 'Exception "Not the owner" has been raised')
}
})

it('sends money to the winner and resets the players array', async () => {
await lotteryInstance.enter({
from: accounts[0],
value: web3.utils.toWei('0.02', 'ether'),
})

await lotteryInstance.enter({
from: accounts[1],
value: web3.utils.toWei('0.03', 'ether'),
})

await lotteryInstance.enter({
from: accounts[2],
value: web3.utils.toWei('0.04', 'ether'),
})

const tx = await lotteryInstance.pickWinner({
from: accounts[0],
})

// Let's grab the event "WinnerPicked" emitted where
// we have the details of this transaction
const { logs } = tx
assert.ok(Array.isArray(logs))
assert.equal(logs.length, 1)

const log = logs[0]
assert.equal(log.event, 'WinnerPicked')

const index = parseInt(log.args.index.toString(10), 10)
const prize = parseFloat(web3.utils.fromWei(log.args.prize.toString(10))) // in ether

assert.equal(prize, 0.09) // 0.02 + 0.03 + 0.04
assert.equal(accounts[index], log.args.winner)

// Make sure the list of players is empty
const players = await lotteryInstance.getPlayers()
assert.equal(0, players.length)
})
})

Frontend

Even though the Frontend part is the bulk of the application, there is no need to spend much time here. It’s a simple Next.js app, one page, no API routes, all client side stuff:

The interesting part here is the connection between Frontend and Blockchain, and how we can interact with it from our UI. This can be done easily thanks to web3.js. I implemented a module with an “nice” API around this library to simplify the methods we need, sort of a façade:

import Web3 from 'web3'
import { EventData } from 'web3-eth-contract'
import lotteryJson from './contract.json'
import emitter from '../emitter'

type Address = string
type Amount = string // amounts are always presented as strings

type ContractDetails = {
owner: Address
players: Address[]
balance: Amount
isManager: boolean
hasEntered: boolean
participants: number
contractBalance: Amount
}

// Network settings have more properties,
// but we're only interested in the `address`
type NetworkSettings = Record<string, { address: Address }>

const projectUrl = process.env['NEXT_PUBLIC_PROJECT_URL']
const networkId = process.env['NEXT_PUBLIC_NETWORK_ID']

const networkSettings = lotteryJson.networks as NetworkSettings

const CONTRACT_ABI = lotteryJson.abi as unknown as AbiItem
const CONTRACT_ADDRESS = networkSettings[networkId ?? 5777].address as Address

export const web3 = new Web3(Web3.givenProvider ?? projectUrl)

// Creates an interface similar to the smart contract
export const lotteryContract = new web3.eth.Contract(
CONTRACT_ABI,
CONTRACT_ADDRESS,
)

// We want to inform to the Frontend when the contract emits
// WinnerPicked event passing the event object, which contains
// all the details sent in the event.
lotteryContract.events.WinnerPicked((error: Error, event: EventData) => {
if (error) {
emitter.emit('error-picking-winner', error)
} else {
emitter.emit('winner-picked', event)
}
})

/**
* @returns contract's deployed address
*/
export function getContractAddress(): Address {
return CONTRACT_ADDRESS
}

/**
* This function will prompt the user for permission to connect their wallet
* @returns list of connected wallet's accounts
*/
export async function requestAccounts(): Promise<Address[]> {
return web3.eth.requestAccounts()
}

/**
* @param address we want to get the balance from
* @returns the amount in that address
*/
export async function getBalance(address: Address): Promise<Amount> {
const balanceWei = await web3.eth.getBalance(address)
return web3.utils.fromWei(balanceWei)
}

/**
* @returns the amount locked in the contract
*/
export async function getContractBalance(): Promise<Amount> {
return getBalance(CONTRACT_ADDRESS)
}

/**
* @returns address of the contract's owner
*/
export async function getContractOwner(): Promise<Address> {
return lotteryContract.methods.owner().call()
}

/**
* @param from address entering the game
* @param ether amount sent to the pool by the participant
*/
export async function enterLottery(
from: Address,
ether: Amount,
): Promise<string> {
return lotteryContract.methods.enter().send({
from,
value: web3.utils.toWei(ether),
})
}

/**
* @param from address querying the list of participants
* @returns list of participants
*/
export async function getPlayers(from: Address): Promise<Address[]> {
if (from) {
return lotteryContract.methods.getPlayers().call({ from })
}

return []
}

/**
* @param from address querying the number of participants
* @returns number of participants
*/
export async function numPlayers(from: Address): Promise<number> {
const players = await getPlayers(from)
return players.length
}

/**
* This funtion can only get called by the manager (owner of the contract).
* It'll pick a random winner and send all the ether there was in the pool
* to this lucky player.
* @param from address calling this function
*/
export async function pickWinner(from: Address): Promise<void> {
if (from) {
return lotteryContract.methods.pickWinner().send({ from })
}
}

/**
* @param address address querying the contract's detail
* @returns a promise with details about the contract
*/
export async function getContractDetails(
address: Address,
): Promise<ContractDetails> {
const [balance, players, owner, contractBalance] = await Promise.all([
getBalance(address),
getPlayers(address),
getContractOwner(),
getContractBalance(),
])

const participants = players.length
const hasEntered = players.includes(address)
const isManager = owner === address

return {
owner,
players,
balance,
isManager,
hasEntered,
participants,
contractBalance,
}
}

Notice we’re importing a json file, contract.json. What is this?, this is the output of our smart contract compilation, the contract artifact. The important part of this json is the ABI, or Application Binary Interface. This is used to create a contract abstraction, which will auto convert all calls into low level ABI calls over RPC for you. The way I like to think of this ABI is like the API in web 2.

Going public

So far we’ve been working and running everything locally, including the blockchain, but our stakeholders are getting nervous, they want to see something, or we simply want to show off our cool dapp. At some point we’ll need/want to go public, right?. Deploying the Frontend is not an issue, we’ve done that millions of times, but how can we deploy our smart contract to a public blockchain?, and the other important question is, if we just want to show the app internally, how can we do so without having to spend real ether every time we deploy and/or interact with the contract?. The answer: Testnets

Testnets are test blockchain networks that act and perform similarly to the main networks (mainnets) they are associated with. Since they operate on separate ledgers from the mainnet, the coins on a testnet have no connection to transactions and value on the mainnet. This allows developers to deploy, test, and execute their projects on a functioning blockchain freely.

For this project I’m deploying the contract in Goerli testnet. In order for me to do that, I need also a node provider. Running your own network node is a real pain in the ass. Not an easy task for various reasons. It would take another article to explain why, but trust me, it’s complex. Instead, you can use some of the available platforms out there, such as Infura, Alchemy or QuickNode. The three of them support Goerli testnet. I created an account with Alchemy, but I have no preference, I just picked the first one I learnt about.

Next step is to add Goerli network to your MetaMask wallet, so you can have accounts and send yourself fake ETH into those accounts, which you can use to deploy your contract in the testnet and test your application as much as you want. You can follow this tutorial to add the network, keeping in mind that the the RPC URL might be different depending on the platform you’re using. For example, in Alchemy the URL looks something like this: https://eth-goerli.g.alchemy.com/v2/YOUR_API_KEY

Then you need to send fake ETH (I call it GoerliETH) into your accounts. Use Goerli Faucet to do so. It’s simple, just copy and paste your account in that textbox and Send Me ETH. You get 0.5 ETH, which is quite enough for you to have some fun.

Last step is deploying your contract and connect your Frontend. Truffle framework, with the help of another package, @truffle/hdwallet-provider, makes this a pice of cake. HDWallet provider is used by truffle when deploying the contract. Take another look at this piece of configuration from truffle-config.js:

// ...
require('dotenv').config()
const HDWalletProvider = require('@truffle/hdwallet-provider')
const { MNEMONIC, NEXT_PUBLIC_PROJECT_URL } = process.env

module.exports = {
// ...
networks: {
// ...
goerli: {
provider: () => new HDWalletProvider(MNEMONIC, NEXT_PUBLIC_PROJECT_URL),
network_id: 5, // Goerli's id
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true, // Skip dry run before migrations? (default: false for public nets )
},
},
// ...
}

When we run a migration,truffle migrate --network goerli (see package.json, npm scripts), truffle will use this provider to grab the gas needed from the first account, based on the MNEMONIC, to deploy the contract on the provided network, NEXT_PUBLIC_PROJECT_URL.

Regarding how the Frontend will communicate with the contract, when running the migration, the contract is compiled generating the contract artifact as we saw previously. The ABI, plus some defined environment variables such as project URL and network ID, will be used to achieve this communication under the hood:

//...
import lotteryJson from './contract.json'

//...
const projectUrl = process.env['NEXT_PUBLIC_PROJECT_URL']
const networkId = process.env['NEXT_PUBLIC_NETWORK_ID']

const networkSettings = lotteryJson.networks as NetworkSettings

const CONTRACT_ABI = lotteryJson.abi as unknown as AbiItem
const CONTRACT_ADDRESS = networkSettings[networkId ?? 5777].address as Address

export const web3 = new Web3(Web3.givenProvider ?? projectUrl)

// Creates an interface similar to the smart contract
export const lotteryContract = new web3.eth.Contract(
CONTRACT_ABI,
CONTRACT_ADDRESS,
)
//...

And that’s pretty much it

I believe I’ve covered the most important parts of the process of building and deploying (at least on a testnet) a simple but full-fledged Web 3.0 application. I’m aware of the fact that I left some details out, and might have been somewhat vague in some of the explanations. There is a lot to take in here and I’m just scratching the surface.

Thoughts

Web2 is sort of broken, and it comes from the fact that Internet is governed by a few big companies. This centralization is causing a couple of problems:

  • Censorship
  • Single point of failure
  • Your data is owned

Let me ask you a question, what do you trust more?

  • Institutions like banks and governments
  • Internet platforms like Meta and Amazon
  • Mathematics

Web3 is trying to fix these problems, solving trust issues with math, building on these core ideas: decentralization, permissionless and trustless. Web3 feels like a natural evolution of the web that hosts decentralized apps that run on blockchain technology, and with a native payment system. But this new version is far from being perfect. It has limitations, although we’re still in a very early stage and I’m confident we’ll figure most of them out.

I hope you enjoyed the article. Smash that 👏 button if you did 😊, and thanks a lot for reading it.

Repo: https://github.com/jscriptcoder/lottery-contract

--

--

Francisco Ramos

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