Road to Web 3 — Buy me a coffee — Reflection Part 1

mitey.titey
7 min readDec 11, 2022

--

For the second week of Road to Web 3, we were challenged to develop a “Buy me a coffee” web application. This is an app that will allow any wallet to tip me ETH (AKA Buy me a coffee) by interacting with a contract deployed to the blockchain.

For this project we used the following platforms;

This week there was additional focus on the fundamentals of both Solidity and JavaScript.

We began the project by setting up a basic folder structure on our local machine. Within that folder we then imported the starting “hardhat” file and folder structure that we would use to develop and test our solidity contracts. To import the hardhat structure we used NPM.

NPM is a software registry. After installing npm you can use the command “npm install” along with the software package name, and the file structure and files are automatically imported to your current directory.

The following two lines turn your empty folder into a full blown JavaScript and Solidity testing environment.

--install hardhat
npm install --save-dev hardhat

--create project
npx hardhat

Upon completion of the commands above, your empty folder will now contain a “contracts” and “scripts” folder. There are other folders created but these were the two we focused on.

The contracts folder is where our solidity (.sol file) contract was stored, and the scripts folder contained our JavaScript (.js file) files. Originally, there was a dummy contract and script file there, but we renamed them and cleared the majority of the code to start.

The way the hardhat development environment works is you can use the JavaScript files to deploy and interact with the contract using a local instance of a blockchain to debug and test. Once you are satisfied with your code you can also use the JavaScript files to deploy to Goerli test network.

Buy Me a Coffee Contract

To develop the contract located in the contracts folder, we constructed four main pieces;

  • Event
  • Scruct
  • Constructor
  • Function

The Event allows us to store data for future reference. In the case of our contract we can store who sent the tx (name and address), when they sent it, and what message they included. We will call this event ONLY when the transaction is successful and someone has bought us a coffee.

    // Event to emit when a Memo is created.
event NewMemo(
address indexed from,
uint256 timestamp,
string name,
string message
);

The Scruct is fairly simple. It just defines an object type that contains multiple values in the contract labeled Memo. This concept was a little confusing at first, but is explained in more detail in the Solidity docs.

    // Memo struct.
struct Memo {
address from;
uint256 timestamp;
string name;
string message;
}

The Constructor is a portion of the contract that ONLY runs when the contract is deployed. This is important for defining variables that you want to remain consistent for the life of the contract. For our example, we defined the variable “owner” as the address that deployed the contract and set it to a “payable” address so we are able to withdraw to it later.

Since the constructor only runs when the contract is created we must also define that the “owner” is payable outside of the constructor.

    constructor() {
// Store the address of the deployer as a payable address.
// When we withdraw funds, we'll withdraw here.
owner = payable(msg.sender);
}

// Address of contract deployer. Marked payable so that
// we can withdraw to this address later.
address payable owner;

The Functions are pieces of our contract that do the work. The three functions we created were buyCoffee, getMemos, and withdrawTips. These functions are able to accept arguments and be modified to restrict who can call them (private or public) and if they are allowed to change the value stored inside of the contract (payable).

In the buyCoffee function we accept a name (ex: mtitus6) and a message (ex: Your awesome!) as arguments. We set the function as public so that anyone can call it and as payable so it allows value to be transferred to the contract.

function buyCoffee(string memory _name, string memory _message) public payable {

In the body of the function we first check if the transaction (or msg) has value associated with it, and if it does then we store the message in the Memo (defined in struct) and call our event, newMemo, to store the the name and message of the buyer.

        // Must accept more than 0 ETH for a coffee.
require(msg.value > 0, "can't buy coffee for free!");

// Add the memo to storage!
memos.push(Memo(
msg.sender,
block.timestamp,
_name,
_message
));

// Emit a NewMemo event with details about the memo.
emit NewMemo(
msg.sender,
block.timestamp,
_name,
_message
);

The next function is getMemos. This function has a simple job, when called it will return all values stored in the Memo object. The function is set as public so any address can call it. In the function, we reference memos which is defined outside of the function equal to Memo[].

 // List of all memos received from coffee purchases.
Memo[] memos;

function getMemos() public view returns (Memo[] memory) {
return memos;
}

The last function is withdrawTips. This function also has a simple job, but a little more important, because this is the function that will allow us to retrieve the value that is stored in the contract. The function references the owner that is defined in our constructor and sends the balance of the contract referred to as address(this).balance. This function can be called by any address, but the value will always be sent to the owner.

    function withdrawTips() public {
require(owner.send(address(this).balance));
}

That completes the contract. 🎉🎉🎉

Buy Me a Coffee Script

In the next step of the process, we develop a JavaScript routine that performs actions against the contract. Last week we used the Remix GUI to deploy and interact with our NFT contract, but this week we do it all using our hardhat environment and JavaScript.

In the beginning of the script we create “helper” functions for accomplishing repetitive tasks such as getting the balance of the contract, getting the balance of any wallet address, and printing text stored within the state of the contract.

In many areas we use use the built-in hardhat objects and methods by referencing the variable “hre” defined in the very beginning of the code. The methods built into hardhat allow us to do simple functions like getting balances, and formatting ETH balances into readable amounts (divide by 10¹⁸).

const hre = require("hardhat");
// Returns the Ether balance of a given address.
async function getBalance(address) {
const balanceBigInt = await hre.ethers.provider.getBalance(address);
return hre.ethers.utils.formatEther(balanceBigInt);
}

// Logs the Ether balances for a list of addresses.
async function printBalances(addresses) {
let idx = 0;
for (const address of addresses) {
console.log(`Address ${idx} balance: `, await getBalance(address));
idx ++;
}
}

// Logs the memos stored on-chain from coffee purchases.
async function printMemos(memos) {
for (const memo of memos) {
const timestamp = memo.timestamp;
const tipper = memo.name;
const tipperAddress = memo.from;
const message = memo.message;
console.log(`At ${timestamp}, ${tipper} (${tipperAddress}) said: "${message}"`);
}
}

The final part of the script is the main function that we will use to perform a number of steps with the contract. All of the above actions are done using a local instance of a blockchain.

The main function performs the following steps;

  • Deploy the contract
  • Get some test wallets
  • Print balances of all wallets and the contract prior to doing anything
  • Test tipping with those wallets
  • Print balances of all wallets and the contract to confirm send and receive
  • Withdraw the tips
  • Print balances again of all wallets and contract to confirm withdraw

To deploy the contract we first define a variable BuyMeACoffee as our contract we built above. We then use a different variable buyMeACoffee (lower case b) to deploy the contract.

After deploying, we wait until the the function method deployed() returns true and emit a message to the log that the contract was successfully deployed.

  // We get the contract to deploy.
const BuyMeACoffee = await hre.ethers.getContractFactory("BuyMeACoffee");
const buyMeACoffee = await BuyMeACoffee.deploy();

// Deploy the contract.
await buyMeACoffee.deployed();
console.log("BuyMeACoffee deployed to:", buyMeACoffee.address);

To set up test wallets, we use variables pre-defined in the hardhat environment (getSigners) and assign to variables owner, tipper, tipper1, tipper2.

  // Get the example accounts we'll be working with.
const [owner, tipper, tipper2, tipper3] = await hre.ethers.getSigners();

To print the balances of our addresses, we use our helper function, printBalances, we created above.

  // Check balances before the coffee purchase.
const addresses = [owner.address, tipper.address, buyMeACoffee.address];
console.log("== start ==");
await printBalances(addresses);

To begin interacting with the contract, we next start sending transactions to our buyCoffee function using the test accounts above.

  
//Define the value of the tip
const tip = {value: hre.ethers.utils.parseEther("1")};

// Buy the owner a few coffees.
await buyMeACoffee.connect(tipper).buyCoffee("Carolina", "You're the best!", tip);
await buyMeACoffee.connect(tipper2).buyCoffee("Vitto", "Amazing teacher", tip);
await buyMeACoffee.connect(tipper3).buyCoffee("Kay", "I love my Proof of Knowledge", tip);

After sending the transactions, we again check the balances.

  // Check balances after the coffee purchase.
console.log("== bought coffee ==");
await printBalances(addresses);

The final step is to withdraw tips using a similar structure that was used to call buyCoffee, but instead we call the withdrawTips function. The withdrawTips function does not accept arguments so we leave the parenthesis empty.

  // Withdraw.
await buyMeACoffee.connect(owner).withdrawTips();

To confirm the withdraw was successful, we again check the balances of the accounts.

  // Check balances after withdrawal.
console.log("== withdrawTips ==");
await printBalances(addresses);

The final step is to run the JavaScript file and confirm it runs as expected. To run the script you must use the console. First navigate to the folder where your project is located (use cd command), then run npx hardhat run scripts/<Your script file>.

If successful, the logs from each step of the script will be printed to the console and you can verify everything functioned as intended.

 npx hardhat run scripts/buy-coffee.js    

Now that we have created the contract, and tested that it functions as expected, the next step is to deploy to the Goerli test network and then design a front end website to interact with it.

I will cover the remaining steps in my next post — Part 2.

--

--