Building a Coin Flip Game on Ethereum (Part 2) - Deploying the Contract
Welcome to part two of the coin flip game series!
In this tutorial, we will guide you through setting up a Hardhat project and deploying a CoinFlip smart contract on the blockchain. Imagine flipping a coin but with Ethereum's robust blockchain technology as your playground. You'll get hands-on with Hardhat, a favourite tool among Ethereum devs for its ease in developing, testing, and deploying smart contracts.
To check out the previous guide on setting up email onboarding with Particle Auth and Biconomy, click here.
Prerequisites
Before you begin, ensure you have the following prerequisites installed:
Node.js and npm are installed on your machine.
Hardhat: Install Hardhat globally using the command
npm install -g hardhat
.
Tutorial: Building and Deploying a Coin Flip Game with Hardhat
1
Set Up the Hardhat Project
1.1
Lock.sol
file to CoinFlip.sol
Your project directory will look like this:2
Install Dependencies
3
Modify CoinFlip.sol
CoinFlip.sol
file.//SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/Strings.sol";
contract CoinFlip {
uint256 public totalMatches;
uint256 public lifetimeValue;
uint256 public minimumAmount = 0.001 ether;
mapping(uint256 => Match) public matches;
struct Match {
address player1;
address player2;
uint256 bet1;
uint256 bet2;
bool complete;
uint256 result;
}
function createMatch() public payable returns (uint256) {
require(msg.value > minimumAmount, "You must send some ether to create a match");
matches[totalMatches].player1 = msg.sender;
matches[totalMatches].bet1 = msg.value;
totalMatches++;
lifetimeValue += msg.value;
return totalMatches - 1;
}
function joinMatch(uint matchId) public payable returns (string memory) {
require(matchId < totalMatches, "Invalid match ID");
require(
matches[matchId].complete == false,
"This match is already complete"
);
require(
msg.value == matches[matchId].bet1,
"You must send the same amount of ether as the bet1 value"
);
matches[matchId].player2 = msg.sender;
matches[matchId].bet2 = msg.value;
lifetimeValue += msg.value;
return flipCoin(matchId);
}
function flipCoin(uint matchId) private returns (string memory) {
require(matchId < totalMatches, "Invalid match ID");
uint randomNumber = uint(block.timestamp) % 2;
if (randomNumber == 0) {
payable(matches[matchId].player1).transfer(
matches[matchId].bet1 + matches[matchId].bet2
);
matches[matchId].complete = true;
matches[matchId].result = 1;
return "The coin came up heads. player 1 wins";
}
else {
payable(matches[matchId].player2).transfer(
matches[matchId].bet1 + matches[matchId].bet2
);
matches[matchId].complete = true;
matches[matchId].result = 2;
return "The coin came up tails, player 2 wins";
}
}
}
4
Hardhat Configuration
hardhat.config.js
file to include the necessary imports. This will also include setting up a networks
section where you will parse a node provider that will aid in deploying the smart contract and a private key for paying gas for deploying the smart contract.5
Deployment Script
deploy.js
inside the scripts directory and replace the script with the following code:5.1
6
Interacting with the Contract
deploy.js
:6.1
Understanding the CoinFlip Smart Contract
Here’s a break down of the various components of the CoinFlip.sol
contract:
Contract Declaration and Importing Libraries
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/Strings.sol";
SPDX-License-Identifier: Identifies the license under which the contract's source code is released.
pragma solidity ^0.8.20: Specifies the version of the Solidity compiler.
import "@openzeppelin/contracts/utils/Strings.sol": Imports the
Strings
library from OpenZeppelin Contracts for string manipulation.
Contract Definition and State Variables
contract CoinFlip {
uint256 public totalMatches;
uint256 public lifetimeValue;
uint256 public minimumAmount = 0.001 ether;
// Make all public matches accessible based on a match ID
mapping(uint256 => Match) public matches;
totalMatches: Public variable tracking the total number of matches played.
lifetimeValue: Public variable tracking the total amount of ether sent in all matches.
minimumAmount: Minimum amount of ether required to create a match.
matches: Mapping to store match details based on match IDs.
Match Struct Definition
struct Match {
address player1;
address player2;
uint256 bet1;
uint256 bet2;
bool complete;
uint256 result;
}
Match Struct: Struct to store details of each match.
createMatch Function
function createMatch() public payable returns (uint256) {
// Require that the player has sent some ether
require(msg.value > minimumAmount, "You must send some ether to create a match");
// Set match details for player1
matches[totalMatches].player1 = msg.sender;
matches[totalMatches].bet1 = msg.value;
// Increment counters
totalMatches++;
lifetimeValue += msg.value;
// Return the match ID
return totalMatches - 1;
}
createMatch Function: Allows a player to create a new match by sending ether.
joinMatch Function
function joinMatch(uint matchId) public payable returns (string memory) {
// Require that the match ID is valid
require(matchId < totalMatches, "Invalid match ID");
// Require that the match is not already complete
require(matches[matchId].complete == false, "This match is already complete");
// Require that the player has sent an amount of ether equal to the bet1 value
require(msg.value == matches[matchId].bet1, "You must send the same amount of ether as the bet1 value");
// Set match details for player2
matches[matchId].player2 = msg.sender;
matches[matchId].bet2 = msg.value;
// Increment lifetimeValue counter
lifetimeValue += msg.value;
// Flip the coin and determine the winner
return flipCoin(matchId);
}
joinMatch Function: Allows a player to join an existing match by sending the same amount of ether as player1. It also automatically calls the flipCoin method and selects a winner, thus completing the gameplay.
flipCoin Function
function flipCoin(uint matchId) private returns (string memory) {
// Require that the match ID is valid
require(matchId < totalMatches, "Invalid match ID");
// Generate a random number between 0 and 1
uint randomNumber = uint(block.timestamp) % 2;
// Determine the winner based on the random number
if (randomNumber == 0) {
// Player1 wins
// Transfer the ether to player1
payable(matches[matchId].player1).transfer(matches[matchId].bet1 + matches[matchId].bet2);
matches[matchId].complete = true;
matches[matchId].result = 1;
return "The coin came up heads. Player 1 wins";
} else {
// Player2 wins
// Transfer the ether to player2
payable(matches
[matchId].player2).transfer(matches[matchId].bet1 + matches[matchId].bet2);
matches[matchId].complete = true;
matches[matchId].result = 2;
return "The coin came up tails. Player 2 wins";
}
}
}
flipCoin Function: Simulates a coin flip by generating a random number (0 or 1) using the current timestamp.
If the number is 0, Player1 wins. The ether is transferred to Player1, and the match is marked as complete.
If the number is 1, Player2 wins. The ether is transferred to Player2, and the match is marked as complete.
This concludes the breakdown of the CoinFlip.sol
contract. Each section of the contract serves a specific purpose, from initializing matches to determining the winner through a simulated coin flip.
Conclusion
Congrats for making it through this tutorial! You've learned how to set up a Hardhat project, deploy a smart contract, and understand the core components of the provided CoinFlip.sol
code. By breaking down the code into sections, we've covered the contract definition, state variables, functions for creating and joining matches, and the coin flip logic. Happy building!