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

mitey.titey
7 min readDec 21, 2022

--

In Part 1 of the project, we developed a Solidity contract that allows anyone to tip us ETH. In addition, they can also share their name and a message that will be stored on chain. Then we developed a JavaScript routine to deploy the contract to a local blockchain to test it out. All of this was done using a local Hardhat environment.

Now that our contract function has been verified, we are ready to deploy it to a public blockchain (Goerli), and build a front end so that our friends can interact with it.

Success 🎉

We have demonstrated in Part 1 that we can deploy the contract locally, but to deploy to Goerli test network it is a little trickier. The biggest hurdle you must overcome is how to interact with your private Goerli wallet without sharing your secrets (wallet private key and API Key).

To keep our secrets safe we create a .env file that stores our private key and the network we want to interact with. To make sure our private key and Alchemy API Key remain secret, we confirm that the .env file is specified in the gitignore which will automatically exclude it from ever accidentally being included in a commit to GitHub.

To create the .env file we open the command line and run the following two commands.

Note: you may need to install the touch library if you get an error.

>> npm install dotenv

>> touch .env

After running these command, you will notice a .env file appear in your project. You can open the file and enter your secrets, which you will need to extract from Alchemy (API Key) and your Metamask wallet (Private Key API).

//Stored in the .env file

GOERLI_URL=https://eth-goerli.alchemyapi.io/v2/<your api key>
GOERLI_API_KEY=<your api key>
PRIVATE_KEY=<your metamask api key>

Now that your secrets are defined it is time to tell Hardhat that we no longer want to interact with our local blockchain and instead want to use Goerli. To do this we must modify the hardhat.config.js file. We cleared the existing code and used the following configuration that uses the secrets we defined above. Now when we reference — network goerli in command line, hardhat will know both the network URL and private key with which to connect.

// hardhat.config.js

require("@nomiclabs/hardhat-ethers");
require("@nomiclabs/hardhat-waffle");
require("dotenv").config()

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

const GOERLI_URL = process.env.GOERLI_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.4",
networks: {
goerli: {
url: GOERLI_URL,
accounts: [PRIVATE_KEY]
}
}
};

Now that we have defined how to connect to the Goerli network, and our secrets are safe, we now create the script that will deploy the contract to the Goerli network. To accomplish this, we created a new Javascript file in the scripts folder, which we titled deploy.js. In this script, we use similar code as when we were testing locally, but since we are only deploying the contract we can ignore the rest.

// scripts/deploy.js

const hre = require("hardhat");

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

await buyMeACoffee.deployed();

console.log("BuyMeACoffee deployed to:", buyMeACoffee.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

To run this script, again we go back to the console and use the following command. Note this time we specify to use the Goerli network we have defined in the config.js file.

npx hardhat run scripts/deploy.js --network goerli

If successful, you will see the address of your deployed contract in the logs. To verify this you can head over to goerli.etherscan and see for yourself.

🎉🎉🎉

PS C:\Users\matt_titus\BuyMeACoffee-contracts> npx hardhat run scripts/deploy.js --network goerli
BuyMeACoffee deployed to: 0x199bd68020a1dbD791935432E335EFbdf057953D

Next step is to create a front end so our friends can easily interact with our contract and buy us a coffee ☕. Alchemy University made creating the front end very easy by allowing us to fork an existing project on Replit.

A sample project URL was provided, https://replit.com/@thatguyintech/BuyMeACoffee-Solidity-DeFi-Tipping-app, and using the Fork button made a copy of the project stored under our personal account.

Replit is essentially a webpage IDE with an added feature of being able to hot reload your webpage to see the results of your code changes. The webpage is also live hosted so during development you can test the page out in various other browsers.

While the webpage was mostly complete, here are the changes that we needed to make:

  • Update the contractAddress in pages/index.js
  • Update the name strings to be your own name in pages/index.js
  • Ensure that the contract ABI matches your contract in utils/BuyMeACoffee.json

The index.js page contains the majority of the code related to the webpage function and design. Some key functions inside of index.js include;

  • Connecting to Metamask to get the account of the user,
  • Creating a transaction to interact with the contract, and
  • Returning the memos from the contract that include the names and messages of the tippers.

In order to connect to Metamask the following code is used. It was explained in the instructional video that this is fairly common code that can be reused across multiple projects. The isWalletConnected function checks the length of accounts to see if a wallet is connected. The accounts variable is set in the connectWallet function which initiates Metamask and allows the user to select an account and connect to the site.

  // Wallet connection logic
const isWalletConnected = async () => {
try {
const { ethereum } = window;

const accounts = await ethereum.request({method: 'eth_accounts'})
console.log("accounts: ", accounts);

if (accounts.length > 0) {
const account = accounts[0];
console.log("wallet is connected! " + account);
} else {
console.log("make sure MetaMask is connected");
}
} catch (error) {
console.log("error: ", error);
}
}

const connectWallet = async () => {
try {
const {ethereum} = window;

if (!ethereum) {
console.log("please install MetaMask");
}

const accounts = await ethereum.request({
method: 'eth_requestAccounts'
});

setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
}

In order to create the transaction we build a buyCoffee function. The function collects the signer from the wallet connection, contract address, and contract ABI and passes those as arguments to define our contract variable, buyMeACoffee.

// Buy Coffee function
const buyCoffee = async () => {
try {
const {ethereum} = window;

if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum, "any");
const signer = provider.getSigner();
const buyMeACoffee = new ethers.Contract(
contractAddress,
contractABI,
signer
);

The contractABI is essentially the decoder for our contract and allows us to reference the contract functions by name as opposed to encoded data. The contractABI definition is defined in the BuyMeACoffee.json file located in the utils folder.

//contractABI
import abi from '../utils/BuyMeACoffee.json';
const contractABI = abi.abi;

After defining the contract, buyMeACoffee, we then create a transaction with the value of 0.001ETH, and call the buyCoffee function from our contract. The wait() function is applied to allow for the transaction to be completed.


console.log("buying coffee..")
const coffeeTxn = await buyMeACoffee.buyCoffee(
name ? name : "anon",
message ? message : "Enjoy your coffee!",
{value: ethers.utils.parseEther("0.001")}
);

await coffeeTxn.wait();

console.log("mined ", coffeeTxn.hash);

console.log("coffee purchased!");
}
} catch (error) {
console.log(error);
}
};

The last step is to return the memos from the contract to our website to display. To display the result was a little more complicated, so I am just going to focus on the interaction with the contract.

Similar to our buyCoffee function we need to collect the details related to our signer, contract address, and contract ABI.

// Function to fetch all memos stored on-chain.
const getMemos = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const buyMeACoffee = new ethers.Contract(
contractAddress,
contractABI,
signer
);

Then instead of calling the buyCoffee function, we invoke the getMemos function to define a memos variable containing our data. Note, we do not need to pass ETH value in a transaction since this is only a read request and does not require value to be transferred.

console.log("fetching memos from the blockchain..");
const memos = await buyMeACoffee.getMemos();
console.log("fetched!");
setMemos(memos);
} else {
console.log("Metamask is not connected");
}

} catch (error) {
console.log(error);
}

That wraps up the Buy Me a Coffee project for Week 2 of the Alchemy Road to Web3 course. This was a much more challenging project with a lot of logic and code to learn and understand. I believe it is important to not only get the code function but to also understand why it is behaving as it does.

If you are interested in looking at my replit code you can see it here.

At the end of this lesson, we have been challenged to modify the contract and code. This is what I will cover in my next post.

  1. Allow your smart contract to update the withdrawal address.
  2. Allow your smart contract to buyLargeCoffee for 0.003 ETH, and create a button on the frontend website that shows a “Buy Large Coffee for 0.003ETH” button.

--

--