# Alchemy的the Road to Web3-第二周文本教程

By [KnowYourself](https://paragraph.com/@knowyourself-2) · 2022-10-14

---

今天我们一起来看看第二周任务。首先看看任务的目标是什么？在本教程中，将学习**如何使用Alchemy、Hardhat、Ethers.js**开发和部署去中心化的“给我买杯咖啡”智能合约，允许访问者发送（假）ETH 作为提示并留下好消息。话不多说，我们现在就开始第二周课程吧。

**1.先决条件**

*   `npm` (`npx`) version 8.5.5
    
*   `node` version 16.13.1
    
*   如果你会js代码更好
    

**2.创建项目**

    #创建一个项目目录并且初始化package.json
    mkdir BuyMeACoffee-contracts
    cd BuyMeACoffee-contracts
    npm init -y
    

![创建目录](https://storage.googleapis.com/papyrus_images/db6c13cc2c94a79108ef0eba0fabff927aa052d6e444ebbcd21398db45362997.png)

创建目录

创建目录

创建目录

创建目录

    # 使用hardhat生成项目框架
    npx hardhat
    # 该命令建议执行，因为依赖可能有问题
    npm install --save-dev hardhat@^2.9.3 @nomiclabs/hardhat-waffle@^2.0.0 ethereum-waffle@^3.0.0 chai@^4.2.0 @nomiclabs/hardhat-ethers@^2.0.0 ethers@^5.0.0
    
    # 当你创建成功后目录如下所示：
    .
    ├── README.md
    ├── contracts
    ├── hardhat.config.js
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── scripts
    └── test
    
    contracts- 您的智能合约所在的文件夹
      在这个项目中，我们将只创建一个，来组织我们的逻辑BuyMeACoffee
    scripts- 您的安全帽 javscript 脚本所在的文件夹
      我们将编写逻辑deploy
      示例脚本buy-coffee
      和一个兑现我们小费的脚本withdraw
    hardhat.config.js- 带有solidity版本和部署设置的配置文件
    

**3.开始开发项目**

我们可以使用任意的vim方式打开上面创建的项目，这里就用VScode了，如果有需要的可以去官网下载

![](https://storage.googleapis.com/papyrus_images/b8feee6b6d3085f26ffbc838d00939211853042eccb90510f93647722039c0c1.png)

我们可以看到生成的目录已经有了一个合约，我们只需要去替换即可，首先将合约文件名替换为 BuyMeACoffee.sol 同时将合约内容替换成下面的。

    //SPDX-License-Identifier: Unlicense
    
    // contracts/BuyMeACoffee.sol
    pragma solidity ^0.8.0;
    
    // Switch this to your own contract address once deployed, for bookkeeping!
    
    contract BuyMeACoffee {
        // 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));
        }
    }
    

![替换后的合约](https://storage.googleapis.com/papyrus_images/cb945fd71e1196786ff586718195c480732b70186462d60695347ceda29ff84b.png)

替换后的合约

替换后的合约

替换后的合约

**4.测试部署合约**

将scripts下面的deploy.js的内容替换为下面的

    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 BuyMeACoffee = await hre.ethers.getContractFactory("BuyMeACoffee");
      const buyMeACoffee = await BuyMeACoffee.deploy();
    
      // Deploy the contract.
      await buyMeACoffee.deployed();
      console.log("BuyMeACoffee deployed to:", buyMeACoffee.address);
    
      // Check balances before the coffee purchase.
      const addresses = [owner.address, tipper.address, buyMeACoffee.address];
      console.log("== start ==");
      await printBalances(addresses);
    
      // Buy the owner a few coffees.
      const tip = {value: hre.ethers.utils.parseEther("1")};
      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);
    
      // Check balances after the coffee purchase.
      console.log("== bought coffee ==");
      await printBalances(addresses);
    
      // Withdraw.
      await buyMeACoffee.connect(owner).withdrawTips();
    
      // Check balances after withdrawal.
      console.log("== withdrawTips ==");
      await printBalances(addresses);
    
      // Check out the memos.
      console.log("== memos ==");
      const memos = await buyMeACoffee.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);
      });
    

当我们将上面的替换完毕后，通过命令行运行JS

    npx hardhat run scripts/deploy.js
    

测试我们的合约，当你执行成功会有下面的现实

    BuyMeACoffee deployed to: 0x2720b4f7e22Ad1c269eee4e91aE2fD1F04c485F6
    == start ==
    Address 0 balance:  9999.998754619375
    Address 1 balance:  10000.0
    Address 2 balance:  0.0
    == bought coffee ==
    Address 0 balance:  9999.998754619375
    Address 1 balance:  9998.999752893990255063
    Address 2 balance:  3.0
    == withdrawTips ==
    Address 0 balance:  10002.998708719732606388
    Address 1 balance:  9998.999752893990255063
    Address 2 balance:  0.0
    == memos ==
    At 1660268820, Carolina (0x70997970C51812dc3A010C7d01b50e0d17dc79C8) said: "You're the best!"
    At 1660268821, Vitto (0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC) said: "Amazing teacher"
    At 1660268822, Kay (0x90F79bf6EB2c4f870365E785982E1f101E93b906) said: "I love my Proof of Knowledge"
    

**5.使用 Alchemy 和 MetaMask 将 BuyMeACoffe.sol 智能合约部署到以太坊 Goerli 测试网**

新建一个deploy01.js，内容如下：

    // scripts/deploy01.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);
      });
    

现在我们的项目整体结构就如下了：

![新的项目结构](https://storage.googleapis.com/papyrus_images/0f7e873e76d6b20215d4f121fc167f5306a470677f5a3d18729b38e07903c43a.png)

新的项目结构

新的项目结构

运行我们刚才脚本：

    npx hardhat run scripts/deploy01.js
    

如果成功的话，你会看到下面这样的提示：

    BuyMeACoffee deployed to: 0x2720b4f7e22Ad1c269eee4e91aE2fD1F04c485F6
    

这里需要注意哦，当我们多次运行的话，你每次都会看到完全相同的部署地址，这是因为当你运行脚本时，Hardhat 工具使用的默认设置是本地开发网络，就在您的计算机上。它快速且具有确定性，非常适合进行一些快速的健全性检查。

但是，为了实际部署到在 Internet 上运行且节点遍布世界各地的测试网络，我们需要更改我们的 Hardhat 配置文件以提供选项。

**6.修改hardhat.config.js 进行配置部署**

首先我们将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_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]
        }
      }
    };
    

    # 安装dotenv
    npm install dotenv
    # 创建一个.env文件
    touch .env
    # 将下面内容写入env中
    GOERLI_URL=https://eth-goerli.alchemyapi.io/v2/<your api key>
    GOERLI_API_KEY=<your api key>
    PRIVATE_KEY=<your metamask api key>
    

此外，为了获得所需的环境变量，可以使用以下资源：

*   `GOERLI_URL`\- 注册一个帐户[炼金术](https://alchemy.com/?a=roadtoweb3weektwo)，创建一个 Ethereum -> Goerli 应用程序，并使用 HTTP URL
    
*   `GOERLI_API_KEY`\- 从您的同一个 Alchemy Ethereum Goerli 应用程序中，您可以获得 URL 的最后一部分，这将是您的 API KEY
    
*   `PRIVATE_KEY`\- 遵循这些[来自 MetaMask 的说明](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key)导出您的私钥。
    

![项目新的结构](https://storage.googleapis.com/papyrus_images/16b1197df1a304b11414bc753a133396919877b8e0771c21598e55ea3dc7dfa6.png)

项目新的结构

项目新的结构

项目新的结构

项目新的结构

获取Goerli测试币，去下面网址获取测试币

运行脚本发布到测试网络

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

当你成功部署页面会显示：

    :BuyMeACoffee-contracts paul$ npx hardhat run scripts/deploy01.js --network goerli
    Compiled 1 Solidity file successfully
    BuyMeACoffee deployed to: 0x2720b4f7e22Ad1c269eee4e91aE2fD1F04c485F6
    

验证下合约是否部署上测试网成功。

**7.实现一个脚本**`withdraw`

我们在刚才的脚本下面新建一个withdraw.js的脚本，内容如下：

    // scripts/withdraw.js
    
    const hre = require("hardhat");
    const abi = require("../artifacts/contracts/BuyMeACoffee.sol/BuyMeACoffee.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="你的合约地址";
      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 buyMeACoffee = 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, buyMeACoffee.address);
      console.log("current balance of contract: ", await getBalance(provider, buyMeACoffee.address), "ETH");
    
      // Withdraw funds if there are funds to withdraw.
      if (contractBalance !== "0.0") {
        console.log("withdrawing funds..")
        const withdrawTxn = await buyMeACoffee.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);
      });
    

在**本地**测试

    # 运行该命令在本地测试
    npx hardhat run scripts/withdraw.js
    

如果没有报错你会有下面的提示：

    current balance of owner:  0.14511094798885063 ETH
    current balance of contract:  0.0 ETH
    no funds to withdraw!
    current balance of owner:  0.14511094798885063 ETH
    

**8.使用 Replit 和 Ethers.js 构建前端 Buy Me A Coffee 网站 dapp**

首先在Replit IDE将下面的仓库进行fork

![fork](https://storage.googleapis.com/papyrus_images/2655963a2d3b5116ac6270995d38a4d466f77c9da7081ed32d38568511b8422a.png)

fork

fork

fork

在fork之后我们会来到自己的工作台，如下所示：

![](https://storage.googleapis.com/papyrus_images/0ce8dba7dcb7cc1da111e9a24f9422b9405b20aff2514885d5174c551336e7c0.png)

将文件中写好的变量进行修改

*   更新输入`contractAddresspages/index.js`
    
*   将名称字符串更新为您自己的名字`pages/index.js`
    
*   确保合同 ABI 与您的合同相匹配`utils/BuyMeACoffee.json`
    

可以看到 contractAddress 变量已经填充了地址。修改成你自己部署的合约

![](https://storage.googleapis.com/papyrus_images/f2c4da8f2224061338f79ddc36d865422f43453dc3173125bbe6da3bf540de91.png)

将复制过来的仓库的Albert修改成你想要的任意名字都可以

![](https://storage.googleapis.com/papyrus_images/5b7a7b43753bf99d252396cb9d7a3ea0b513a0d13ce5f918905d1fb3d0beadeb.png)

将刚才在编译器生成的ABI复制到Replit中的utils/BuyMeACoffee.json

![](https://storage.googleapis.com/papyrus_images/00a5001493cb5898ce2ffcbcb767a3737da52610432420d966797654d53e4dd3.png)

![运行项目](https://storage.googleapis.com/papyrus_images/61642e001ec23929f19756724499261a5887475a5d5f601027d3430bbd03366d.png)

运行项目

运行项目!

---

*Originally published on [KnowYourself](https://paragraph.com/@knowyourself-2/alchemy-the-road-to-web3)*
