# Alchemy Road to Web3第二周部署买咖啡Dapp **Published by:** [Licrazy](https://paragraph.com/@doger/) **Published on:** 2022-08-25 **URL:** https://paragraph.com/@doger/alchemy-road-to-web3-dapp ## Content TL;DR 本文主要由六部分组成,分别为:准备、搭建环境、代码编译、代码测试、部署Dapp、发布与提交。 一、准备: 1、结合之前前序文档,创建好本周的github代码库以及同步到replit.com上。 如图,依次点击右上角github账号--“repositories”--填写“name”(随意)--“create repository”(切记勾选“add a README file”不然会是一个空库,无法导入replit) 2、登陆你的replit账号,依次点击“create”--“import”,然后选择上一步创建的代码库“magweek2”,再次点击“import from github”,完成代码的导入工作 3、进去项目后,如下图所示,点击“Done” 二、搭建环境 1、点击右边“Shell”,首先输入mkdir BuyABCACoffee-contracts并回车(ABC3个字母处,可改为任意名字,后文涉及到ABC处的均可替换为自己定义的名字,也可以不改~); 再次输入cd BuyABCACoffee-contracts并回车。 2、输入npm init -y并回车。 3、输入npx hardhat并回车。(中间大约需要点5次回车,就是那几个√的地方需要按回车才能继续) 大概成功安装后会如下图所示: 4、输入npm install --save-dev @nomiclabs/hardhat-waffle 并回车,等待安装完成。(中间各种warn不要管) 5、输入npm install dotenv并回车,等待安装完成。至此环境搭建步骤已经完成。 三、项目代码编译 1、点开contracts目录,把Lock.sol重命名(rename)为BuyABCACoffee.sol(注意,文件名最好与前面创建的目录前缀一致) 2、点击刚才改名的“BuyABCACoffee”文件,将里面的内容替换为如下代码: (特别注意ABC可以改成自己设置的名字,需与前面保持一致。 记得保存文件内容) // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; // Import this file to use console.log import "hardhat/console.sol"; // Switch this to your own contract address once deployed, for bookkeeping! // Example Contract Address on Goerli: 0xDBa03676a2fBb6711CB652beF5B7416A53c1421D contract BuyABCACoffee { // Event to emit when a Memo is created. event NewMemo( address indexed from, uint256 timestamp, string name, string message ); // Memo struct. struct Memo { address from; uint256 timestamp; string name; string message; } // Address of contract deployer. Marked payable so that // we can withdraw to this address later. address payable owner; // List of all memos received from coffee purchases. Memo[] memos; constructor() { // Store the address of the deployer as a payable address. // When we withdraw funds, we'll withdraw here. owner = payable(msg.sender); } /** * @dev fetches all stored memos */ function getMemos() public view returns (Memo[] memory) { return memos; } /** * @dev buy a coffee for owner (sends an ETH tip and leaves a memo) * @param _name name of the coffee purchaser * @param _message a nice message from the purchaser */ function buyCoffee(string memory _name, string memory _message) public payable { // 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 ); } /** * @dev send the entire balance stored in this contract to the owner */ function withdrawTips() public { require(owner.send(address(this).balance)); } } 2、在scripts目录鼠标右键或者左键点击3个点处,选“Add file”,输入buycoffee.js回车来新建文件。 3、在“buycoffee.js”文件里输入以下代码。(同样,ABC可以修改,需与前面保持一致。若前面改了,此处修改会较多) 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}"`); } } async function main() { // Get the example accounts we'll be working with. const [owner, tipper, tipper2, tipper3] = await hre.ethers.getSigners(); // We get the contract to deploy. const BuyABCACoffee = await hre.ethers.getContractFactory("BuyABCACoffee"); const buyABCACoffee = await BuyABCACoffee.deploy(); // Deploy the contract. await buyABCACoffee.deployed(); console.log("BuyABCACoffee deployed to:", buyABCACoffee.address); // Check balances before the coffee purchase. const addresses = [owner.address, tipper.address, buyABCACoffee.address]; console.log("== start =="); await printBalances(addresses); // Buy the owner a few coffees. const tip = {value: hre.ethers.utils.parseEther("1")}; await buyABCACoffee.connect(tipper).buyCoffee("Carolina", "You're the best!", tip); await buyABCACoffee.connect(tipper2).buyCoffee("Vitto", "Amazing teacher", tip); await buyABCACoffee.connect(tipper3).buyCoffee("Kay", "I love my Proof of Knowledge", tip); // Check balances after the coffee purchase. console.log("== bought coffee =="); await printBalances(addresses); // Withdraw. await buyABCACoffee.connect(owner).withdrawTips(); // Check balances after withdrawal. console.log("== withdrawTips =="); await printBalances(addresses); // Check out the memos. console.log("== memos =="); const memos = await buyABCACoffee.getMemos(); printMemos(memos); } // 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); }); 完成后如下图所示: 4、点击deploy.js文件,将原文替换成以下代码。 // scripts/deploy.js const hre = require("hardhat"); async function main() { // We get the contract to deploy. const BuyABCACoffee = await hre.ethers.getContractFactory("BuyABCACoffee"); const buyABCACoffee = await BuyABCACoffee.deploy(); await buyABCACoffee.deployed(); console.log("BuyABCACoffee deployed to:", buyABCACoffee.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); }); 5、在scripts目录左键点击右侧3个点处,选“Add file”,新建withdraw.js文件,并回车。 6、在上一步新建的“withdraw.js”文件里,输入以下代码。 // scripts/withdraw.js const hre = require("hardhat"); const abi = require("../artifacts/contracts/BuyABCACoffee.sol/BuyABCACoffee.json"); async function getBalance(provider, address) { const balanceBigInt = await provider.getBalance(address); return hre.ethers.utils.formatEther(balanceBigInt); } async function main() { // Get the contract that has been deployed to Goerli. const contractAddress="0x314Cb061D81759F339c6F026c86D09Ad528A31b5"; const contractABI = abi.abi; // Get the node connection and wallet connection. const provider = new hre.ethers.providers.AlchemyProvider("goerli", process.env.GOERLI_API_KEY); // Ensure that signer is the SAME address as the original contract deployer, // or else this script will fail with an error. const signer = new hre.ethers.Wallet(process.env.PRIVATE_KEY, provider); // Instantiate connected contract. const buyABCACoffee = new hre.ethers.Contract(contractAddress, contractABI, signer); // Check starting balances. console.log("current balance of owner: ", await getBalance(provider, signer.address), "ETH"); const contractBalance = await getBalance(provider, buyABCACoffee.address); console.log("current balance of contract: ", await getBalance(provider, buyABCACoffee.address), "ETH"); // Withdraw funds if there are funds to withdraw. if (contractBalance !== "0.0") { console.log("withdrawing funds..") const withdrawTxn = await buyABCACoffee.withdrawTips(); await withdrawTxn.wait(); } else { console.log("no funds to withdraw!"); } // Check ending balance. console.log("current balance of owner: ", await getBalance(provider, signer.address), "ETH"); } // 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); }); 7、左键点击BuyABCACoffee-contracts右边三个点,选“Add file”,新建.env文件(注意,此文件最前面有小数点)。 8、先暂时将以下代码输入.env文件 GOERLI_URL=https://eth-goerli.alchemyapi.io/v2/ GOERLI_API_KEY= PRIVATE_KEY= 9、登陆alchemy账号并创建ETH-Goerli的API(如何创建见 前序文章 第4步) 点击VIEW KEY,然后复制下图所示API KEY,粘贴到上一步的GOERLI_API_KEY=后面;复制HTTPS,粘贴到上一步的GOERLI_URL=后面。 再去metamask钱包导出钱包私钥(如何导出见 前序文章 第5步,最好新建一个新钱包),将私钥粘贴到上一步的PRIVATE_KEY=后面。 10、点击hardhat.config.js文件,并将原文替换成以下代码: // 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 = process.env.GOERLI_URL; const PRIVATE = process.env.PRIVATE_KEY; //console.log(GOERLI); /** * @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.9", networks: { goerli: { url: GOERLI, accounts: [PRIVATE] } } }; 四、代码完成,开始项目测试 1、又回到右边栏了,点击“Shell”,若观察回到上一层目录了,请执行cd BuyABCACoffee-contracts (同样ABC为你应与前面你设置的名字一样**)**切换到子目录中。没有则不管。以后遇到都需要这么处理,即cd +文件名 。如下图: 然后输入npx hardhat run scripts/buycoffee.js并回车。(中间会遇到有一次”?直接回车,以后遇到该问题也是如此操作)如下图: 当运行完成后,看图可发现我们的控制台又回到了上一层目录,同理我们通过输入cd BuyABCACoffee-contracts回到子目录。 2、输入npx hardhat run scripts/deploy.js并回车。(中间会遇到有一次”?直接回车,以后遇到该问题也是如此操作) 当出现如下图:deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3,则代表运行成功。当然,这只是尝试部署,还没真正到链上。 3、输入npx hardhat run scripts/deploy.js --network goerli并回车。 若出现下图:INSUFFICIENT_FUNDS代码,则代表钱包每天Goerli网上的ETH。去https://www.goerlifaucet.com 并登陆Alchemy 帐户以获得一些免费的测试币。 正常成功的部署应该是如下图所示,会出现deployed to:后面的地址就是我们部署的合约地址,复制下来后续步骤会要用到。 这里有的朋友可能等很久都不会出现合约地址,可以试着按下回车,或者直接去自己的钱包查看链上是否有合约部署记录。 4、输入npx hardhat run scripts/withdraw.js并回车(中间可能会有一次回车)。 五、部署dapp 1、先回到上一层magweek2(你们命名的可能不一样),输入cd 并回车(如果已经回到上一层目录了,就不用管了),然后输入cd magweek2。 输入npx create-next-app buyabcacoffee-dapp,并回车 (此目录名可以自命名,后续需保持与此一致。中间可能有一次回车。 出现SUCCESS表示成功。 2、输入cd buyabcacoffee-dapp并回车,然后输入npm install --save ethers并回车。 3、安装成功后,左边栏不一定及时出现新目录,需要手动刷新。 (1)先点左边第二个按钮version control,直到出现红框出的buyabcacoffee即可。 (2)再点回第一个按钮file,等待更新目录,会等几分钟。 4、为了让dapp找到合约,需要把BuyABCACoffee产生的合约属性文件拷贝到dapp目录下。操作流程如下:(ABC如果前面有改,则是你们改的名字) (1)点开BuyABCACoffee-contracts目录,选择artifacts/contract/BuyABCACoffee.sol这个目录,选择其中叫BuyABCACoffee.json的文件,先复制其里面的内容到其他地方(比如新建一个记事本拷贝进去),点BuyABCACoffee.json文件右边的三个点,选“rename“,复制其文件名,如图 (2)点击buyabcacoffee-dapp目录右边的三个点,选择“Add folder“,新建utils文件夹。 (3)点击utils右边三个点,选择“Add file“,新建BuyABCACoffee.json文件,再将之前复制的内容拷贝到该文件,即完成。 5、打开pages目录,点击index.js文件,将原文用以下代码替换掉。 注意:①下面ABC几处同样需要和前面保持一致。②第19行代码“contractAddress”处填上第四大步第3点里面记录的合约地址。③名字(hymnmark有3处)可以改为任意名字。④@FinderTechnical处可以改成自己的twitter号。 import abi from '../utils/BuyABCACoffee.json'; import { ethers } from "ethers"; import Head from 'next/head' import Image from 'next/image' import React, { useEffect, useState } from "react"; import styles from '../styles/Home.module.css' export default function Home() { // Contract Address & ABI const contractAddress = "0x2a112B58765c93B0151C10D0Aa54a70698859676"; const contractABI = abi.abi; // Component state const [currentAccount, setCurrentAccount] = useState(""); const [name, setName] = useState(""); const [message, setMessage] = useState(""); const [memos, setMemos] = useState([]); const onNameChange = (event) => { setName(event.target.value); } const onMessageChange = (event) => { setMessage(event.target.value); } // 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); } } const buyCoffee = async () => { try { const { ethereum } = window; if (ethereum) { const provider = new ethers.providers.Web3Provider(ethereum, "any"); const signer = provider.getSigner(); const buyABCACoffee = new ethers.Contract( contractAddress, contractABI, signer ); console.log("buying coffee..") const coffeeTxn = await buyABCACoffee.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!"); // Clear the form fields. setName(""); setMessage(""); } } catch (error) { console.log(error); } }; // 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 buyABCACoffee = new ethers.Contract( contractAddress, contractABI, signer ); console.log("fetching memos from the blockchain.."); const memos = await buyABCACoffee.getMemos(); console.log("fetched!"); setMemos(memos); } else { console.log("Metamask is not connected"); } } catch (error) { console.log(error); } }; useEffect(() => { let buyABCACoffee; isWalletConnected(); getMemos(); // Create an event handler function for when someone sends // us a new memo. const onNewMemo = (from, timestamp, name, message) => { console.log("Memo received: ", from, timestamp, name, message); setMemos((prevState) => [ ...prevState, { address: from, timestamp: new Date(timestamp * 1000), message, name } ]); }; const { ethereum } = window; // Listen for new memo events. if (ethereum) { const provider = new ethers.providers.Web3Provider(ethereum, "any"); const signer = provider.getSigner(); buyABCACoffee = new ethers.Contract( contractAddress, contractABI, signer ); buyABCACoffee.on("NewMemo", onNewMemo); } return () => { if (buyABCACoffee) { buyABCACoffee.off("NewMemo", onNewMemo); } } }, []); return (
"{memo.message}"
From: {memo.name} at {memo.timestamp.toString()}