The Complete Guide to Building a Full-Stack Web3 Dapp on Base L2
Get started and build a full stack web3 dapp on Coinbase’s powerful L2, . Follow this tutorial and check out all the code for this project .
We’ll start with , a statically-typed programming language designed for implementing on Ethereum & EVM-compatible blockchains. We’ll use to deploy our smart contract to , a testnet that provides a sandbox environment for developers to test their dapps for Base.
To interact with blockchain data, we’ll use a on , a decentralized protocol for indexing and querying blockchain data. Subgraphs are open APIS that allow us to efficiently extract the data we need from the Base Sepolia blockchain. We’ll harness the power of to interact with the blockchain and our application's front-end will be built using .
By the end of this tutorial, you will:
- Fully understand these technologies.
- Use them to create a fully functional web3 dapp.
Whether you're a seasoned developer looking to transition into web3 or an eager beginner ready to dive into decentralized applications, this tutorial will provide you with the knowledge and skills you need to start building your own web3 projects immediately.
Ether Coin Flip Overview
The dapp is called Ether Coin Flip. It is a simple betting game where two players can wager on the outcome of a “coin flip.” Here’s how it works:
- Player 1 calls the
newCoinFlip()
function with any amount of Ether - A new coin flip is created, assigned an ID and it gets added to the EtherCoinFlipStructs mapping
- Player 2 calls the
endCoinFlip()
function by passing the coin flip’s ID, and is required to use the same amount of Ether is player 1 - The smart contract uses pseudo-randomness to select a winner of the coin flip
- All the Ether is sent to the winner of the coin flip
It’s important to note that this is for tutorial purposes only and should not be deployed to mainnet. While pseudo-randomness can be useful for this tutorial, this smart contract could be manipulated by node validators if real funds were being used. In order to use randomness in a secure and verifiable way, consider using .
How to write a smart contract on Base
There are a number of powerful tools that enable you to write, test, and deploy smart contracts. We’ll be using , a web-based integrated development environment (IDE).
Before we deep dive into the contract, let's identify the license and declare the version of Solidity we’d like to use:
Next, we can initialize the contract. We declare the EtherCoinFlip contract and all the Solidity code inside this contract will be inside these brackets.
To establish the basic structure of the game with all the necessary components, we can define a struct named EtherCoinFlipStruct
. This struct will represent the details of each coin flip and will be mapped to a unique coin flip ID. The struct should contain the following information:
- The public key addresses of both players
- The wager amount
- The total amount of Ether involved
- The addresses of the winner and the loser
- A bool representing if the coin flip is active or not
We can initialize a uint256
variable for the number of coin flips. For the sake of clarity, we can call it numberOfCoinFlips
and set it to 1 to keep track of the total number of coin flips that have happened.
Next, we can create a mapping to link every coin flip ID to its corresponding EtherCoinFlipStruct
. This way, a coin flip ID will be associated with all the correct details.
Now before we start writing the main functions that will power Ether Coin Flip, we first need to create that emit the necessary data. These events will be important later on when we build a subgraph on The Graph. If you’re unfamiliar with the difference between events & functions, you can read .
We can declare two events: StartedCoinFlip and FinishedCoinFlip. These events will emit information about the coin flip when the respective function finishes. We’ll make sure that only the most important information is being emitted. For the StartedCoinFlip event, we can emit the following:
- The coin flip ID
- Who started the coin flip
- The wager
- Whether it is active or not
We can write the StartedCoinFlip event like this:
Next, we’ll need the FinishedCoinFlip event, which will emit the following:
- The coin flip ID
- The winner of the coin flip
- The loser
- The coin flip’s activity status (which will now be set to false)
We can write the FinishedCoinFlip
like this:
Now that we've set up the basic structure of our EtherCoinFlip contract, it's time to implement the core functionality that will allow players to start and end coin flips. We'll create three main functions:
newCoinFlip()
- Allows a player to start a new coin flip.endCoinFlip()
- Allows another player to end a coin flip. The smart contract determines the winner and distributes the total Ether to them.getActiveCoinFlips()
- Returns a list of all active coin flips.
Let's explore each of these functions, understand how they work, and clarify the Solidity code involved!
Starting a Coin Flip
The newCoinFlip()
function allows a player to start a new coin flip by sending Ether to the contract. Here's how it's defined:
Notice how this function is both public
**and** payable
. This means that anyone can view it and also send Ether to it. The function will return the coin flip’s ID so that the player can identify their coin flip.
Let’s walk through what is happening in this function:
- The wallet address sending the Ether (msg.sender) is being assigned to player1
- The
coinFlipID
is being assigned an ID that is 1 more than the current number of coin flips (numberOfCoinFlips++) - The
coinFlipID
is the key associated with the struct we wrote earlier (EtherCoinFlipStructs
) and assigned values for the available details. - Our
StartedCoinFlip()
event is emitting thecoinFlipID
, player1, the wager in Ether, and theisActive
boolean is being assigned true.
Ending a Coin Flip
Next, we can explore the endCoinFlip()
function. Here it is:
Again, the function is public
and payable
.
Let's walk through what is happening in this function:
- Retrieve the Coin Flip Struct: The function accesses the coin flip associated with the provided
coinFlipID
from theEtherCoinFlipStructs
mapping. - Check if the Coin Flip is Active: It ensures that the coin flip hasn't already been finished.
- Assign Player 2: The address of the caller (
msg.sender
) is assigned toplayer2
. - Validate the Wager Amount: It checks that the amount of Ether sent (
msg.value
) is within 1% of the starting wager, ensuring both players have similar stakes. - Verify the Coin Flip ID: It confirms that the provided
coinFlipID
matches the ID stored in the struct. - Update the Coin Flip Struct with Player 2's Details: Assigns
player2
tobetEnder
, records their wager, and calculates the total Ether involved. - Generate Pseudo-Randomness: Creates a pseudo-random hash using blockchain properties and the caller's address to determine the winner.
- Determine the Winner and Loser: Based on the random result, it assigns the winner and loser.
- Transfer Ether to the Winner: Sends the total Ether from both wagers to the winner's address.
- Deactivate the Coin Flip: Sets the
isActive
flag tofalse
to indicate the coin flip is finished. - Emit the
FinishedCoinFlip
Event: Emits an event with the coin flip ID, winner's address, loser's address, and the updatedisActive
status.
Getting Active Coin Flips
The getActiveCoinFlips()
function allows anyone to retrieve a list of all active coin flips. Here's how it's defined:
This function is public
and view
, meaning it can be called externally and doesn't modify the state of the contract. It returns an array of EtherCoinFlipStruct
representing the active coin flips.
Let's walk through what is happening in this function:
- Initialize Active Coin Flip Counter: Starts by initializing a counter
activeCount
to zero. - Count the Active Coin Flips: Iterates through all coin flips from ID 1 up to
numberOfCoinFlips - 1
and incrementsactiveCount
for each active coin flip. - Create an Array to Hold Active Coin Flips: Creates a new in-memory array
activeFlips
with a size equal to the number of active coin flips. - Initialize an Index for the Array: Initializes a variable
currentIndex
to zero to track the array index. - Populate the Array with Active Coin Flips: Iterates through the coin flips again, adding each active coin flip to the
activeFlips
array and incrementingcurrentIndex
. - Return the Array of Active Coin Flips: Returns the
activeFlips
array containing all active coin flips.
By understanding these functions, you're now ready to use the Ether Coin Flip smart contract! You can start new coin flips, end coin flips, and retrieve active coin flips—all essential for building the front-end of your dapp.
To wrap up this section, here is the complete smart contract:
Next, we can begin testing the smart contract on testnet using Ethereum Remix!
Testing your Base smart contract in Ethereum Remix
Now that we have our smart contract code ready, it's time to test it and deploy it to the Base Sepolia testnet. We'll use Ethereum Remix, a powerful web-based IDE for Ethereum smart contract development. In this section, we'll cover:
- Testing the smart contract on Remix
- Deploying the contract to Base Sepolia using an injected Web3 provider (MetaMask)
- Interacting with the deployed contract using Remix's UI and your crypto wallet
Prerequisites
Before we begin, ensure you have the following:
- A crypto wallet installed on your web browser (, , , etc.)
- Base Sepolia set up on MetaMask ()
- Testnet Ether on the Base Sepolia network ()
You can also go ahead and paste the smart contract into a new Solidity file.
- Change the Solidity compiler to match the version of Solidity we used to write our smart contract and hit the “Compile EtherCoinFlip.sol” button!
- Once you set the compiler to the correct version, and clicked compile, you’re ready to deploy it to testnet! This can be done with Remix’s built in testing feature, which allows you to .
We are going to test the smart contract using injected provider MetaMask.
- Once you have your MetaMask connected, you can click the “Deploy” button to deploy
EtherCoinFlip.sol
to the blockchain!
Make sure your , and then approve the transaction.
- Congratulations, you have deployed a smart contract to Base Sepolia! You can begin interacting with the smart contract under the “Deployed Contracts” tab, which will allow you to call the contract’s functions:
- When starting new coin flips with the
newCoinFlip()
function, make sure you assign a value in Ether, which will be the wager (or msg.value).
Once you have started a few coin flips, you can also end coin flips from another wallet. Simply call the endCoinFlip()
function with the correct amount in the value field, and the correct coin flip ID.
Once you are satisfied with your smart contract, it is time to start building a subgraph to query blockchain data from our dapp.
Building a subgraph on Base Sepolia
Click the “Create a Subgraph” button to get started and name your subgraph.
Once you’ve created your Subgraph in Subgraph Studio, you can create your subgraph easily using the commands on the lower right of the page. Commands include CLI installation, initializing your subgraph, and deploying to Subgraph Studio!
1. Install the Graph CLI
You must have and a package manager of your choice (npm
, yarn
or pnpm
) installed to use the Graph CLI.
On your local machine, run one of the following commands:
2. Create your subgraph
Subgraph Studio lets you create, manage, deploy, and publish subgraphs, as well as create and manage API keys.
Go to and connect your wallet.
Click "Create a Subgraph". It is recommended to name the subgraph in Title Case: "Subgraph Name Chain Name".
3. Initialize your subgraph
The following command initializes your subgraph from an existing contract:
Note: If your contract was verified on Etherscan, then the ABI will automatically be created in the CLI. Otherwise, you will need to provide the ABI while using the CLI.
You can find commands for your specific subgraph on the subgraph page in .
When you initialize your subgraph, the CLI will ask you for the following information:
- Protocol: Choose the protocol your subgraph will be indexing data from.
- Subgraph slug: Create a name for your subgraph. Your subgraph slug is an identifier for your subgraph.
- Directory: Choose a directory to create your subgraph in.
- Ethereum network (optional): You may need to specify which EVM-compatible network your subgraph will be indexing data from.
- Contract address: Locate the smart contract address you’d like to query data from.
- ABI: If the ABI is not auto-populated, you will need to input it manually as a JSON file.
- Start Block: You should input the start block to optimize subgraph indexing of blockchain data. Locate the start block by finding the block where your contract was deployed.
- Contract Name: Input the name of your contract.
- Index contract events as entities: It is suggested that you set this to true, as it will automatically add mappings to your subgraph for every emitted event.
- Add another contract (optional): You can add another contract.
See the following screenshot for an example for what to expect when initializing your subgraph:
4. Write your subgraph
The init
command in the previous step creates a scaffold subgraph that you can use as a starting point to build your subgraph. By default, your subgraph will index all emitted events.
When making changes to the subgraph, you will mainly work with three files:
- Manifest (
subgraph.yaml
) - defines what data sources your subgraph will index. - Schema (
schema.graphql
) - defines what data you wish to retrieve from the subgraph. - AssemblyScript Mappings (
mapping.ts
) - translates data from your data sources to the entities defined in the schema.
For a detailed breakdown on how to write your subgraph, check out .
5. Deploy your subgraph
Remember, deploying is not the same as publishing.
- When you deploy a subgraph, you push it to , where you can test, stage and review it.
- When you publish a subgraph, you are publishing it onchain to the decentralized network.
- Once your subgraph is written, run the following commands:
- Authenticate and deploy your subgraph. The deploy key can be found on the subgraph's page in Subgraph Studio.
The CLI will ask for a version label.
It's strongly recommended to use , e.g. 0.0.1
. That said, you can choose any string for the version such as: v1
, version1
, asdf
, etc.
6. Review your subgraph
If you’d like to examine your subgraph before publishing it to the network, you can use to do the following:
- Run and test sample queries
- Analyze your subgraph in the dashboard to check information.
- Check the logs on the dashboard to see if there are any errors with your subgraph. The logs of an operational subgraph will look like this:
7. Publish your subgraph to The Graph Network
Publishing a subgraph to the decentralized network makes it available to query with 100,000 free queries per month, which is perfect for hobby developers and hackathon projects.
Publishing with Subgraph Studio
- To publish your subgraph, click the Publish button in the dashboard.
- Select the network to which you would like to publish your subgraph.
Adding curation signal to your subgraph
- To attract Indexers to query your subgraph, you should add GRT curation signal to it.
- This improves quality of service, reduces latency, and enhances network redundancy and availability for your subgraph.
- If eligible for indexing rewards, Indexers receive rewards based on the signaled amount.
- It’s recommended to curate your subgraph with at least 3,000 GRT to attract 3 Indexers. Check reward eligibility based on subgraph feature usage and supported networks.
To learn more about curation, read .
To save on gas costs, you can curate your subgraph in the same transaction you publish it by selecting this option:
Now, you can query your subgraph by sending GraphQL queries to its Query URL, which you can find by clicking the Query button.
How to build a Front-End Application with React and ethers.js
Now that we've deployed our smart contract and can easily query data from our subgraph on The Graph, it's time to build a user-friendly front-end application. We'll use for the UI and to interact with the blockchain.
In this section, we'll cover:
- Setting up a React application
- Connecting to the blockchain (Base Sepolia in our case) using MetaMask
- Interacting with the smart contract using ethers.js to start a coin flip
- Displaying active coin flips with our subgraph and allowing users to participate
Prerequisites
- Node.js and npm installed on your machine
- Basic understanding of React.js
- Familiarity with JavaScript and
Setting Up the React Application
1. Initialize a New React Project
Use create-react-app
on the command line to bootstrap your project:
This should create a fresh React project in your IDE:
Navigate into the project directory:
2. Install Required Dependencies
We'll need several packages to interact with the blockchain and handle data fetching:
- ethers: Library for interacting with the Ethereum blockchain.
- graphql and graphql-request: For querying The Graph's API.
- @tanstack/react-query: For managing server state in React applications.
Most of the app will be built inside the App.js
file with additional functionality added in with a Dashboard.js
component later.
First, we'll take a look at the App.js
file and explore each section of the code.
Building a React app using Ethers
- Imports:
- contractAddress: The address where your
EtherCoinFlip
contract is deployed on Base Sepolia. - abi: The ABI of your contract imported from
ABI.json
.
- baseSepoliaChainId: The chain ID for the Base Sepolia network.
- baseSepoliaParams: Network parameters required to add or switch to Base Sepolia in MetaMask.
- provider: An ethers.js provider to interact with the blockchain.
- signer: An ethers.js signer representing the user's wallet.
Function: switchToBaseSepolia
- Purpose: Ensures the user's MetaMask is connected to the Base Sepolia network.
- Steps:
- Get Current Chain ID: Fetches the chain ID of the currently connected network.
- Check if Already Connected: Compares it with
baseSepoliaChainId
. - Switch Network: If not connected, attempts to switch to Base Sepolia.
- Switch Chain: Uses
wallet_switchEthereumChain
. - Add Network: If the network is not added (
error code 4902
), it adds the network usingwallet_addEthereumChain
.
- Switch Chain: Uses
- Error Handling: Logs errors if switching or adding the network fails.
Component: App
- State Variables:
- isConnected: Tracks if the user's wallet is connected.
- contractInstance: Stores the initialized contract instance for interaction.
Function: connectWallet
- Purpose: Initiates wallet connection and provider setup.
- Steps:
- Initialize Provider: Calls
initializeProvider
. - Update State: Sets
isConnected
totrue
upon success. - Error Handling: Logs any errors during the connection process.
Function: initializeProvider
- Purpose: Sets up the ethers.js provider and signer, connects to the smart contract.
- Steps:
- Check MetaMask Installation: Verifies if
window.ethereum
is available. - Create Provider: Initializes a new
Web3Provider
using MetaMask's provider. - Request Accounts: Prompts the user to connect their wallet.
- Get Signer: Retrieves the signer representing the user's account.
- Initialize Contract: Creates a new contract instance with the ABI and signer.
- Switch Network: Calls
switchToBaseSepolia
to ensure the correct network is selected. - Store Contract: Updates
contractInstance
in the state for later use. - Error Handling: Alerts the user if MetaMask is not installed.
Component: StartCoinFlipButton
- Purpose: Provides an interface for users to start a new coin flip by entering a wager amount.
- Props:
- contract: The initialized contract instance passed from the
App
component.
- contract: The initialized contract instance passed from the
- State:
- wager: The amount of Ether the user wants to wager.
- Purpose: Calls the
newCoinFlip
function on the smart contract with the specified wager. - Steps:
- Check Contract Initialization: Ensures the contract is available.
- Log Action: Outputs debug information to the console.
- Execute Transaction: Calls
newCoinFlip
with the wager converted to Wei. - Wait for Confirmation: Waits for the transaction to be mined.
- Error Handling: Logs any errors during the transaction.
Render Function for StartCoinFlipButton
- Components:
- Input Field: Allows the user to enter the wager amount in Ether.
value
: Bound to thewager
state variable.onChange
: Updates thewager
state when the input changes.
- Button: Triggers the
startCoinFlip
function when clicked.
- Input Field: Allows the user to enter the wager amount in Ether.
Render Function of App
- Components:
- Header: Displays the title of the application.
- Conditional Rendering:
- Not Connected: Shows a "Connect Wallet" button if the user is not connected.
- Connected: Renders the
StartCoinFlipButton
andDashboard
components when the wallet is connected.
And that is everything inside the App.js
file! Here is all of it together:
Now that we have finished the App.js
file, it is time to create the Dashboard component for displaying active coin flips. This page should contain:
- The ability to start coin flips
- A dashboard displaying active coin flips (using our subgraph on The Graph)
- A button to end any given coin flip
It will also display a button for users to “end” a coin flip, with the ether amount & coin flip ID automatically used inside the function call, making the game simple and accessible.
Designing a Dashboard in React
- Imports:
- React: Core library for building UI components.
- useQuery: Hook from
@tanstack/react-query
for data fetching. - gql, request: Functions from
graphql-request
for making GraphQL queries. - ethers: Library for blockchain interactions.
GraphQL Query
- Purpose: Defines a GraphQL query to fetch the latest 10 started and finished coin flips from The Graph.
- Entities Queried:
- startedCoinFlips: Active coin flips that have been initiated.
- finishedCoinFlips: Coin flips that have been completed.
GraphQL API URL
- Purpose: Retrieves the subgraph API URL from environment variables.
Component: Dashboard
- Props:
- contract: The initialized contract instance passed from the
App
component.
- contract: The initialized contract instance passed from the
- useQuery Hook:
- queryKey: Identifies the query for caching purposes.
- queryFn: Async function that executes the GraphQL query.
- Returned Values:
- data: The data fetched from the API.
- status: The status of the query (
loading
,error
,success
).
Function: endCoinFlip
- Purpose: Allows the user to join an active coin flip by calling
endCoinFlip
on the smart contract. - Steps:
- Check Contract Initialization: Ensures the contract is available.
- Parse Inputs:
- coinFlipIDInt: Converts the
coinFlipID
to an integer. - wagerValue: Converts the
startingWager
to a BigNumber in Wei.
- coinFlipIDInt: Converts the
- Alert User: Notifies the user about the action being taken.
- Execute Transaction: Calls
endCoinFlip
with the appropriate parameters. - Wait for Confirmation: Waits for the transaction to be mined.
- Retrieve Coin Flip Details: Fetches the updated coin flip data from the contract.
- Determine Outcome: Checks if the current user is the winner or loser.
- Alert User of Outcome: Displays a message indicating whether the user won or lost.
- Error Handling: Logs any errors and alerts the user.
Function: getActiveCoinFlips
- Purpose: Processes the fetched data to obtain a list of active coin flips.
- Steps:
- Check Data Availability: Returns an empty array if data is not available.
- Extract Started and Finished Flips: Gets arrays of started and finished coin flips.
- Create Set of Finished IDs: Uses a
Set
for efficient lookup of finished coin flip IDs. - Filter Active Flips: Removes any started coin flips that have been finished.
- Result: Returns an array of active coin flips.
Render Function
- Conditional Rendering:
- Loading State: Displays a loading message while the data is being fetched.
- Error State: Shows an error message if the query fails.
- Success State:
- Active Coin Flips Available: Renders a table listing all active coin flips.
- No Active Coin Flips: Displays a message indicating that there are no active coin flips.
- Table Structure:
- Headers: Defines the columns for coin flip details.
- Rows: Iterates over
activeCoinFlips
to display each coin flip.- Coin Flip ID: The unique identifier of the coin flip.
- Bet Starter: Address of the player who started the coin flip.
- Wager: The wager amount formatted in Ether.
- Action: A button to end the coin flip, calling
endCoinFlip
. - Transaction: A link to view the transaction on the Block Explorer.
Running the React Application
In order to run the website locally, complete the following steps:
- Start the development server
- In your browser, open http://localhost:3000
- Connect your wallet
- Start a new coin flip by entering the wager amount & clicking the start coin flip button
- End a coin flip by selecting one from the dashboard and clicking the end coin flip button
Now, your should have the site running locally and connected to Base Sepolia via ethers.js!
Final Thoughts
The world of decentralized applications is vast and full of opportunities. With the knowledge and experience gained from this tutorial, you're well-equipped to innovate and contribute to the evolving web3 ecosystem.
About The Graph
is the source of data and information for the decentralized internet. As the original decentralized data marketplace that introduced and standardized subgraphs, The Graph has become web3’s method of indexing and accessing blockchain data. Since its launch in 2018, tens of thousands of developers have for dapps across 70+ blockchains - including Ethereum, Solana, Arbitrum, Optimism, Base, Polygon, Celo, Fantom, Gnosis, and Avalanche.
As demand for data in web3 continues to grow, The Graph enters a with a more expansive vision including new data services and query languages, ensuring the decentralized protocol can serve any use case - now and into the future.
Discover more about how The Graph is shaping the future of decentralized physical infrastructure networks (DePIN) and stay connected with the community. Follow The Graph on , , , , , and . Join the community on The Graph’s , join technical discussions on The Graph’s .
oversees The Graph Network. The Graph Foundation is overseen by the . , , , , , and are eight of the many organizations within The Graph ecosystem.